内存分配与回收策略

Java技术体系中提倡的自动内存管理最终可以归结为自动化的解决了两个问题:给对象分配内存以及回收分配给对象的内存。
对象的内存分配,往大方面讲,就是在堆上分配(也可能经过JIT编译后被拆解为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

  • 对象优先在Eden分配
  • Minor GC和Full GC一样吗
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 动态对象年龄判定
  • 空间担保分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
虚拟机提供了-XX:+PrintGCDetails 这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出时输出当前的内存各区域分配情况

/**
 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 */
public class TestAllocation {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB]; // 此处发生一次GC
    }

}

输出结果如下:

[GC (Allocation Failure) [DefNew: 7835K->400K(9216K), 0.0060142 secs] 7835K->6544K(19456K), 0.0060542 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4660K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  52% used [0x00000007bec00000, 0x00000007bf029140, 0x00000007bf400000)
  from space 1024K,  39% used [0x00000007bf500000, 0x00000007bf564278, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)
 Metaspace       used 3193K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 354K, capacity 388K, committed 512K, reserved 1048576K

代码中产生分配三个2MB和一个4MB大小的对象,在运行时可以通过-Xms20m -Xmx20m -Xmn10m这三个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。 -XX:SurvivorRatio=8 决定了新生代中Eden区和一个Survivor区的空间比例为8:1,从输出结果 eden space 8192K, from space 1024K, to space 1024K可以看出,新生代总可用空间为9216KB(Eden区+1个Survivor区的总容量)
执行上诉代码的 allocation4 = new byte[4 * _1MB] 会发生一次Minor GC,这次GC的结果是新生代7835K变成了400K,而总内存几乎没有减少,(因为其余的三个对象是还是存活的,虚拟机几乎没有找到可回收的对象)。此次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内容,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间,所以只要通过分配担保机制提前转移到老年代里面去。
而这次GC结束后,4MB的allocation4对象顺利分配在Eden区,因此程序执行完的结果是Eden占4MB,Survivor空闲,老年代被占用6MB。GC日志则证明了这一点。

Minor GC和Full GC一样吗?
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC): 指发生在老年代的GC,出现了Major GC,经常会伴随最少一次的Minor GC(但也并非是绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Minor GC的策略选择过程),Major GC的速度一般会比Minor GC慢10倍以上。

大对象直接进入老年代
所谓的大对象是指,需要大量连续内存对象的Java空间,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息(但更坏的是那种朝生夕死的短命大对象,写程序应该要积极避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置他们。
虚拟机提供了一个 -XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制。

/**
 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * -XX:PretenureSizeThreshold=3145728
 */
public class TestAllocation {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation;
        allocation = new byte[4 * _1MB];
    }
}

可以看出输出结果为:

Heap
 def new generation   total 9216K, used 1855K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  22% used [0x00000007bec00000, 0x00000007bedcfeb8, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
 Metaspace       used 3191K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 354K, capacity 388K, committed 512K, reserved 1048576K

我们看到Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的allocation对象直接被分配在老年代中,这是因为-XX:PretenureSizeThreshold被设置为3MB,因此超过3MB的对象都会直接在老年代进行分配。但需要注意的是,XX:PretenureSizeThreshold参数只对Serial和PreNew两款收集器有效,Parallel Scavenge收集器下不认识这个参数,Parallel Scavenge 收集器一般不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew+CMS收集器的组合。

长期存活的对象将进入老年代
虚拟机采用了分代回收的思想来管理内存,那么内存回收时就必须能识别哪些对象应该放在新生代,哪些对象应该放在老年代中。为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并且进过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设置为1.对象在Survivor区中每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定层度(默认到15岁),就将会被晋升到老年代中。对象晋升的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold设置

/**
 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 */
public class TestAllocation {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }
}

如果将 -XX:MaxTenuringThreshold设置为1
结果如下:

[GC (Allocation Failure) [Tenured: 8192K->4755K(10240K), 0.0025583 secs] 10147K->4755K(19456K), [Metaspace: 3187K->3187K(1056768K)], 0.0028851 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 246K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,   3% used [0x00000007bec00000, 0x00000007bec3d890, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 8851K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  86% used [0x00000007bf600000, 0x00000007bfea4e08, 0x00000007bfea5000, 0x00000007c0000000)
 Metaspace       used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 356K, capacity 388K, committed 512K, reserved 1048576K

如果将 -XX:MaxTenuringThreshold设置为15
结果如下:

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:     679264 bytes,     679264 total
: 6247K->663K(9216K), 0.0045887 secs] 6247K->4759K(19456K), 0.0046213 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:       1360 bytes,       1360 total
: 4843K->1K(9216K), 0.0015428 secs] 8939K->4746K(19456K), 0.0015648 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4316K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  52% used [0x00000007bec00000, 0x00000007bf036d20, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400550, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 4745K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  46% used [0x00000007bf600000, 0x00000007bfaa2500, 0x00000007bfaa2600, 0x00000007c0000000)
 Metaspace       used 3273K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K

可以看到, from space 的大小为0%,说明该内存中的对象已经被移动到了老年代。这跟预期的不符。
经过研究:我所使用的jdk版本是1.8,而《深入理解Java虚拟机》用的是1.6.后期JVM对该参数进行过优化。且,JVM会对对象进行动态年龄判断,也就是并不是一定要达到XX:MaxTenuringThreshold设置的值,才会把对象放入老年代(虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。)。
为做验证,我们把代码中的 _1MB 改成 2 * 1024 * 1024,在做如下的JVM参数设置 -Xms20m -Xmx20m -Xmn10m
输出结果如下:

[GC (Allocation Failure) [DefNew
Desired survivor size 1048576 bytes, new threshold 15 (max 15)
- age   1:     939096 bytes,     939096 total
: 10681K->917K(18432K), 0.0076453 secs] 10681K->9109K(38912K), 0.0076751 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 1048576 bytes, new threshold 15 (max 15)
- age   1:       2208 bytes,       2208 total
- age   2:     924736 bytes,     926944 total
: 9272K->905K(18432K), 0.0012610 secs] 17464K->9097K(38912K), 0.0012795 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 18432K, used 9862K [0x00000007bd800000, 0x00000007bec00000, 0x00000007bec00000)
  eden space 16384K,  54% used [0x00000007bd800000, 0x00000007be0bf3a0, 0x00000007be800000)
  from space 2048K,  44% used [0x00000007be800000, 0x00000007be8e24e0, 0x00000007bea00000)
  to   space 2048K,   0% used [0x00000007bea00000, 0x00000007bea00000, 0x00000007bec00000)
 tenured generation   total 20480K, used 8192K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000)
   the space 20480K,  40% used [0x00000007bec00000, 0x00000007bf400010, 0x00000007bf400200, 0x00000007c0000000)
 Metaspace       used 3273K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K

可见,这次的运行结果是符合我们的预期的。
对于上述的例子做一个总结,该代码中,allocation1对象需要256KB内存,Survivor可以容纳,当XX:MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后非常的干净。而XX:MaxTenuringThreshold=15时,第二次GC后,allocation1对象仍然还留在新生代Survivor空间,这是新生代仍有404KB被占用。

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

/**
 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
 */
public class TestAllocation {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        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];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
}

输出结果如下

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:     941736 bytes,     941736 total
: 6339K->919K(9216K), 0.0047523 secs] 6339K->5015K(19456K), 0.0047819 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:       1744 bytes,       1744 total
: 5098K->1K(9216K), 0.0017241 secs] 9194K->5002K(19456K), 0.0017455 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4317K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  52% used [0x00000007bec00000, 0x00000007bf037040, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf4006d0, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 5000K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  48% used [0x00000007bf600000, 0x00000007bfae21f8, 0x00000007bfae2200, 0x00000007c0000000)
 Metaspace       used 3273K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K

虽然设置了 MaxTenuringThreshold=15,会发现运行结果中Survivor的空间占用仍然为0%,而老年代与预期增加了,也就是说,allocation1和allocation2 对象都直接进入了老年代,而没有订到15岁的临界年龄。因为这两个对象加起来已经达到了512KB,并且他们是同年的,满足同年对象达到Survivor空间的一般规则。因此直接到了老年代。这个跟上一个章节点讲到的内容是一致的。

空间担保分配
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure设置值是否允许担保失败。如果允许,那么则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这个Minor GC是有风险的;如果小于,或者HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次FullGC。

什么是冒险:新生代采用了复制收集算法,但为了内存利用率,只使用其中一种Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。而老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间作比较,决定是否进行Full GC来让老年代腾出更多的空间。
然而取平均值进行比较仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(handle promotion failure)。如果后面出现了HandlerPromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlerPromotionFailure开关打开,避免Full GC过于频繁。
在JDK6之后,JVM保证只要老年代的连续空间大于新生代对象总代小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值