内存分配与回收策略

内存分配与回收策略

  Java技术体系中的自动内存管理最终可以归结为自动化地解决两个问题:给对象分配内存和回收分配给对象的内存。关于内存回收这一点,我们在Java垃圾收集机制中详细介绍了各种回收算法以及JVM中常见的收集器。接下来我们主要看看JVM是如何给对象分配内存的。

  对象的内存分配,往大的方向上说就是在堆上的分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配,这是JIT即时编译器的优化,我们不做介绍),对象主要分配在新生代(Eden和Survivor)的Eden区上。少数情况下也可能会直接分配在老年代(Tenured)中,其分配的细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。接下来我们将通过例子讲解几条最普遍的内存分配规则。

对象优先在Eden分配

  大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行内存分配时,虚拟机将发起一次Minor GC(新生代内存回收)。其中虚拟机通过参数 -XX:+PrintGCDetails打印垃圾收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志。我们执行代码如下,其中虚拟机参数设置为-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8。

public class Main {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        /**
         * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
         * -Xms20M 设置堆大小为20M   -Xmx20M 避免堆自动扩展         -Xmn10M  设置年轻代大小 
         * -XX:+PrintGCDetails 打印日志信息
         * -XX:SurvivorRatio=8  设置Eden和Survivor大小比值
         */
        // TODO Auto-generated method stub
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[6 * _1MB]; //新生代需要Minor GC,Full GC直接将其分配进OldGen
        allocation4 = new byte[4 * _1MB]; //新生代分配内存
    }

}

//打印日志如下

1、[GC-- [PSYoungGen: 4932K->4932K(9216K)] 11076K->13124K(19456K), 0.0050819 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2、[Full GC [PSYoungGen: 4932K->2511K(9216K)] [ParOldGen: 8192K->8192K(10240K)] 13124K->10703K(19456K) [PSPermGen: 2551K->2550K(21504K)], 0.0123169 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
3、Heap
4、PSYoungGen total 9216K, used 6774K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
5、eden space 8192K, 82% used [0x00000000ff600000,0x00000000ffc9d840,0x00000000ffe00000)
6、from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
7、to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
8、ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
9、object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400020,0x00000000ff600000)
10、PSPermGen total 21504K, used 2557K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
11、object space 21504K, 11% used [0x00000000f9a00000,0x00000000f9c7f688,0x00000000faf00000)

  为了解释方便,给日志加上了行号,首先第1行,GC说明发生了垃圾回收。其中[PSYoungGen: 4932K->4932K(9216K)]表示新生代应用Parallel Scavenger收集器,垃圾收集前内存占用4932K,垃圾收集后内存占用4932K,因为allocation1和allocation2对象都还存活;该区域总内存为9216K(9M)。其中11076K->13124K(19456K)表示Java堆内存回收前后占用内存和总内存。其中0.0050819 secs表示执行内存回收时间。其中[Times: user=0.00 sys=0.00, real=0.00 secs],user表示用户态消耗的CPU时间,sys表示内核态消耗的CPU时间,real表示操作开始到结束所经过的墙钟时间。同理第2行表示为:新生代执行了Full GC,内存变化为由4932K变为2511K,说明有2M内存进入老年代;老年代采用Parallel Old收集器,新创建的allocation3同样进入老年代,可以看到老年代内存占用为8192K(8M);永久代(PermGen)内存没有太大变化。第3-11行是程序执行完内存的情况新生代内存总共9M,内存占用6774K(最后allocation4直接分配在新生代中),新生代Eden区8192K,占用82%;from和to区域1024K,占用0%;老年代内存总共10M,占用8M(2M+6M)。其中永久代内存总共21M(21504K),占用2557K。

大对象直接进入老年代

  大对象是指需要大量连续内存空间的Java对象,如字符串以及数组。虚拟机提供参数-XX:PretenureSizeThreshold参数来指定大对象,大于该值的对象都是大对象。如下我们指定大于3M的对象都是大对象,可以从打印日志中看出allocation3直接被分配到老年代中。程序如下:

public class Main {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        /**
         * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
         * -Xms20M 设置堆大小为20M   -Xmx20M 避免堆自动扩展         -Xmn10M  设置年轻代大小 
         * -XX:+PrintGCDetails 打印日志信息
         * -XX:SurvivorRatio=8  设置Eden和Survivor大小比值
         * --XX:+UseSerialGC 使用Serial + Serial Old收集器组合进行内存回收
         * -XX:PretenureSizeThreshold=3145728  指定超过3M的对象直接进入老年代
         */
        // TODO Auto-generated method stub
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[1 * _1MB];
        //allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[6 * _1MB]; //新生代需要Minor GC,Full GC
        //allocation4 = new byte[4 * _1MB];
    }
}

//打印日志如下
Heap
 def new generation   total 9216K, used 2024K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  24% used [0x00000000f9a00000, 0x00000000f9bfa028, 0x00000000fa200000)
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
 tenured generation   total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  60% used [0x00000000fa400000, 0x00000000faa00010, 0x00000000faa00200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2558K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07f9c8, 0x00000000fb07fa00, 0x00000000fc2c0000)
No shared spaces configured.

长期存活的对象将进入老年代

  既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活且能被Survivor容纳的话,将被移动到Survivor空间,并且对象年龄设为1,对象在Survivor区中每经过一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认是15岁),就会晋升到老年代。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。还是看一段代码和日志信息吧!

public class Main {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        /**
         * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
         * -Xms20M 设置堆大小为20M   -Xmx20M 避免堆自动扩展         -Xmn10M  设置年轻代大小 
         * -XX:+PrintGCDetails 打印日志信息
         * -XX:SurvivorRatio=8  设置Eden和Survivor大小比值
* -XX:MaxTenuringThreshold 当年龄大于该值时,放入老年代
*/ // TODO Auto-generated method stub byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; allocation2 = new byte[4 * _1MB]; allocation3 = new byte[5 * _1MB]; //新生代执行Minor GC,将allocation1移入Survivor,将allocation2移入老年代 allocation3 = null; allocation3 = new byte[4 * _1MB]; //直接在新生代上分配空间 } } //打印日志信息 [GC[DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 737904 bytes, 737904 total : 5188K->720K(9216K), 0.0044078 secs] 5188K->4816K(19456K), 0.0044833 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] [GC[DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 136 bytes, 136 total : 5925K->0K(9216K), 0.0028463 secs] 10021K->4816K(19456K), 0.0029261 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4260K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000) eden space 8192K, 52% used [0x00000000f9a00000, 0x00000000f9e28fd0, 0x00000000fa200000) from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200088, 0x00000000fa300000) to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000) tenured generation total 10240K, used 4816K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000) the space 10240K, 47% used [0x00000000fa400000, 0x00000000fa8b4050, 0x00000000fa8b4200, 0x00000000fae00000) compacting perm gen total 21248K, used 2558K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000) the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb07fa00, 0x00000000fb07fa00, 0x00000000fc2c0000) No shared spaces configured.

  如上中的日志信息中,第一个红色字体(5188K->720K(9216K))说明将allocation1放入了Survivor区,第二个红色字体(5925K->0K)说明将新生代内存清空,其中allocation3对象的值为null,直接清除内存,而处于Survivor区中的allocation1直接移入老年代。最后堆中内存分配情况为allocation1和allocation2都在老年代,而allocation3在新生代中。大家可自行调大-XX:MaxTenuringThreshold的值,再次观察日志信息。

动态对象年龄判定

  为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须到达MaxTenuringThreshold才能晋升到老年代,如果Survivor空间中相同年龄对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

public class Main {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        /**
         * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution -XX:+UseSerialGC
         * -Xms20M 设置堆大小为20M   -Xmx20M 避免堆自动扩展         -Xmn10M  设置年轻代大小 
         * -XX:+PrintGCDetails 打印日志信息
         * -XX:SurvivorRatio=8  设置Eden和Survivor大小比值
         */
        // TODO Auto-generated method stub
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];  //Minor GC 将allocation1和allocation2放入Survivor,将allocation3放入老年代,allocation4放入新生代
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
}

//打印日志
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1000064 bytes,    1000064 total
: 5280K->976K(9216K), 0.0055251 secs] 5280K->5072K(19456K), 0.0055829 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] 
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:        136 bytes,        136 total
: 5236K->0K(9216K), 0.0017304 secs] 9332K->5072K(19456K), 0.0017616 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4260K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  52% used [0x00000000f9a00000, 0x00000000f9e28fd0, 0x00000000fa200000)
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200088, 0x00000000fa300000)
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
 tenured generation   total 10240K, used 5072K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  49% used [0x00000000fa400000, 0x00000000fa8f4060, 0x00000000fa8f4200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2558K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07fa10, 0x00000000fb07fc00, 0x00000000fc2c0000)
No shared spaces configured.

空间分配担保

  在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有内存空间,如果条件成立,那么Minor GC可以确保是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许那么会继续检查老年代最大可用连续内存空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,但本次Minor GC存在风险;如果小于或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC,让老年代腾出更多的空间。但是在JDK6 Update24之后,HandlePromotionFailure参数将不会影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码可以发现虽然还定义了该参数,但是代码中已不使用它了。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

posted @ 2017-03-31 16:06  best.lei  阅读(1029)  评论(1编辑  收藏  举报