JVM内存分配与回收策略

来源:互联网 发布:吕雉与戚夫人知乎 编辑:程序博客网 时间:2024/06/05 08:47

在基于Serial/Serial Old收集器(ParNew /Serial Old收集器组合基本也符合)下的的内存回收策略。

对象优先分配在Eden区域:
在大多数情况下,新生代的对象在Eden区重分配,当Eden区域没有足够的空间分配时,虚拟机会进行一次Minor GC.

V M参数:-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution

public static void main(String[] args)    {      final int tenMB = 10* 1024 * 1024;        byte[] alloc1, alloc2, alloc3;        alloc1 = new byte[tenMB / 5];          alloc2 = new byte[5 * tenMB];        alloc3 = new byte[4 * tenMB];        alloc3 = null;        alloc3 = new byte[6 * tenMB];    }

运行结果:

[GC [DefNewDesired survivor size 5242880 bytes, new threshold 15 (max 15)- age   1:    2237152 bytes,    2237152 total: 54886K->2184K(92160K), 0.0508477 secs] 54886K->53384K(194560K), 0.0508847 secs] [Times: user=0.03 sys=0.03, real=0.06 secs][GC [DefNewDesired survivor size 5242880 bytes, new threshold 15 (max 15)- age   2:    2237008 bytes,    2237008 total: 43144K->2184K(92160K), 0.0028660 secs] 94344K->53384K(194560K), 0.0028957 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]Heapdef new generation   total 92160K, used 65263K [0x1a1d0000, 0x205d0000, 0x205d0000)  eden space 81920K,  77% used [0x1a1d0000, 0x1df69a10, 0x1f1d0000)  from space 10240K,  21% used [0x1f1d0000, 0x1f3f2250, 0x1fbd0000)  to   space 10240K,   0% used [0x1fbd0000, 0x1fbd0000, 0x205d0000)tenured generation   total 102400K, used 51200K [0x205d0000, 0x269d0000, 0x269d0000)   the space 102400K,  50% used [0x205d0000, 0x237d0010, 0x237d0200, 0x269d0000)compacting perm gen  total 12288K, used 360K [0x269d0000, 0x275d0000, 0x2a9d0000)   the space 12288K,   2% used [0x269d0000, 0x26a2a3c0, 0x26a2a400, 0x275d0000)    ro space 8192K,  66% used [0x2a9d0000, 0x2af20f10, 0x2af21000, 0x2b1d0000)    rw space 12288K,  52% used [0x2b1d0000, 0x2b8206d0, 0x2b820800, 0x2bdd0000)

*长期存活的对象将进入老年代*
演示年轻代对象晋级到年老代的过程。 这里设置MaxTenuringThreshold=1。前面不设置的时候,默认MaxTenuringThreshold取值15。当设置不同的阈值,jvm在内存处理会有不同。我们重点观察观察alloc1 这么小块区域在不同的MaxTenuringThreshold参数设置下的遭遇。

这时候JVM的参数中加上MaxTenuringThreshold=1如下:

-verbose:gc  -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
[GC [DefNewDesired survivor size 5242880 bytes, new threshold 1 (max 1)- age   1:    2237152 bytes,    2237152 total: 54886K->2184K(92160K), 0.0641037 secs] 54886K->53384K(194560K), 0.0641390 secs] [Times: user=0.03 sys=0.03, real=0.06 secs][GC [DefNewDesired survivor size 5242880 bytes, new threshold 1 (max 1): 43144K->0K(92160K), 0.0036114 secs] 94344K->53384K(194560K), 0.0036418 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]Heapdef new generation   total 92160K, used 63078K [0x1a1d0000, 0x205d0000, 0x205d0000)  eden space 81920K,  77% used [0x1a1d0000, 0x1df69a10, 0x1f1d0000)  from space 10240K,   0% used [0x1f1d0000, 0x1f1d0000, 0x1fbd0000)  to   space 10240K,   0% used [0x1fbd0000, 0x1fbd0000, 0x205d0000)tenured generation   total 102400K, used 53384K [0x205d0000, 0x269d0000, 0x269d0000)   the space 102400K,  52% used [0x205d0000, 0x239f2260, 0x239f2400, 0x269d0000)compacting perm gen  total 12288K, used 360K [0x269d0000, 0x275d0000, 0x2a9d0000)   the space 12288K,   2% used [0x269d0000, 0x26a2a3c0, 0x26a2a400, 0x275d0000)    ro space 8192K,  66% used [0x2a9d0000, 0x2af20f10, 0x2af21000, 0x2b1d0000)    rw space 12288K,  52% used [0x2b1d0000, 0x2b8206d0, 0x2b820800, 0x2bdd0000)

. *大对象直接进入老年代*
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

大对象对虚拟机的内存分配来说就是一个坏消息(比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(复习一下:新生代采用复制算法收集内存)。

private static final int _1MB = 1024 * 1024;   /**   * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8  * -XX:PretenureSizeThreshold=3145728  */  public static void testPretenureSizeThreshold() {    byte[] allocation;    allocation = new byte[4 * _1MB];  //直接分配在老年代中  
Heap  def new generation   total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)  eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000)  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)  the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)  compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)  the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)  No shared spaces configured. 

执行代码中的testPretenureSizeThreshold()方法后,我们看到Eden空间几乎没有被使用,而老年代的10MB空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为PretenureSizeThreshold被设置为3MB(就是3145728,这个参数不能像-Xmx之类的参数一样直接写3MB),因此超过3MB的对象都会直接在老年代进行分配。

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

private static final int _1MB = 1024 * 1024;  /**   * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15  * -XX:+PrintTenuringDistribution   */  @SuppressWarnings("unused")  public static void testTenuringThreshold2() {   byte[] allocation1, allocation2, allocation3, allocation4;    allocation1 = new byte[_1MB / 4];       // allocation1+allocation2大于survivo空间一半    allocation2 = new byte[_1MB / 4];      allocation3 = new byte[4 * _1MB];    allocation4 = new byte[4 * _1MB];    allocation4 = null;    allocation4 = new byte[4 * _1MB];  } 
[GC [DefNew  Desired Survivor size 524288 bytes, new threshold 1 (max 15)  age   1:     676824 bytes,     676824 total   5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]   [GC [DefNew  Desired Survivor size 524288 bytes, new threshold 15 (max 15)  : 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   Heap  def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)  1eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)  the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)  compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)  No shared spaces configured. 

执行代码中的testTenuringThreshold2()方法,并设置-XX:MaxTenuringThreshold=15,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,也就是说,allocation1、allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个对象加起来已经到达了512KB,并且它们是同年的,满足同年对象达到Survivor空间的一半规则。我们只要注释掉其中一个对象new操作,就会发现另外一个就不会晋升到老年代中去了。

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

下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁.