HotSpot中Parallel Scavenge/Parallel Old与Serial/Serial Old内存分配策略区别

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。

关于JVM的垃圾收集技术,前面的文章中已经介绍虚拟机中的垃圾收集器体系以运作原理,现在我们再一起来探讨一下给对象分配内存的那点事儿。

对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配 ),对象主要分配在新生代的Eden区上。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

接下来我们将会讲解几条最普遍的内存分配规则,并通过代码去验证这些规则。接下来的代码的是在使用Parallel Scavenge/Parallel Old 收集器下与Serial/Serial Old收集器下的内存分配和回收的策略的对比.

注意:Serial/Serial Old收集器与ParNew/Serial Old收集器组合的规则也基本一致

验证对象优先分配在新生代的Eden区中:

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

/** *VM参数:-verbose:gc-Xms20M-Xmx20M-Xmn10M-XX:+PrintGCDetails -XX:SurvivorRatio=8 */
public class SeeMinorGC {
     private static final int _1MB= 1024*1024;

    public static void testAllocation(){

    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];//是否会出现一次Minor GC

    }

    public static void main(String args[]) {
           testAllocation();
    }
}

estAllocation()方法中,尝试分配3个2MB大小和1个4MB大小的对象,在运行时通过-Xms20M、-Xmx20M、-Xmn10M这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1

Parallel Scavenge/Parallel Old收集器下的GC日志:

Heap
 PSYoungGen      total 9216K, used 6799K [0xe6600000, 0xe7000000, 0xe7000000)
  eden space 8192K, 83% used [0xe6600000,0xe6ca3fc0,0xe6e00000)
  from space 1024K, 0% used [0xe6f00000,0xe6f00000,0xe7000000)
  to   space 1024K, 0% used [0xe6e00000,0xe6e00000,0xe6f00000)
 ParOldGen       total 10240K, used 4096K [0xe5c00000, 0xe6600000, 0xe6600000)
  object space 10240K, 40% used [0xe5c00000,0xe6000010,0xe6600000)
 Metaspace       used 1619K, capacity 2202K, committed 2328K, reserved 4400K

从输出的结果也可以清晰地看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216KB(Eden区+1个Survivor区的总容量)。

执行testAllocation()时,allocation1、allocation2、allocation3三个对象顺利的被分配在了eden区。当分配allocation4对象的时候,发现新生代剩余的空间已经不足以存放allocation4对象,所以该对象直接被分配在老年代。此时并不会发生GC。

对于上述代码,Serial/Serial Old收集下的GC日志为:

[GC[DefNew:6651K->148K(9216K),0.0070106 secs]6651K->6292K(19456K),
0.0070426 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4326K[0x029d0000,0x033d0000,0x033d0000)
eden space 8192K,51%used[0x029d0000,0x02de4828,0x031d0000)
from space 1024K,14%used[0x032d0000,0x032f5370,0x033d0000)
to space 1024K,0%used[0x031d0000,0x031d0000,0x032d0000)
tenured generation total 10240K,used 6144K[0x033d0000,0x03dd0000,0x03dd0000)
the space 10240K,60%used[0x033d0000,0x039d0030,0x039d0200,0x03dd0000)
compacting perm gen total 12288K,used 2114K[0x03dd0000,0x049d0000,0x07dd0000)
the space 12288K,17%used[0x03dd0000,0x03fe0998,0x03fe0a00,0x049d0000)
No shared spaces configured.

分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代6651KB变为148KB,而总内存占用量则几乎没有减少(因为allocation1、allocation2、allocation3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的4MB内存,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。

这次GC结束后,4MB的allocation4对象顺利分配在Eden中,因此程序执行完的结果是
Eden占用4MB(被allocation4占用),Survivor空闲,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。

如果将上述testAllocation()改为如下则会在PS中发生MinorGC:

byte[] allocation1,allocation2,allocation3,allocation4;
    allocation1 = new byte[2*_1MB];
    allocation2 = new byte[2*_1MB];
    allocation3 = new byte[2*_1MB];
    allocation4 = new byte[3*_1MB];//出现一次Minor GC

此时在Parallel Scavenge/Parallel Old下运行该方法,GC日志如下:

[GC (Allocation Failure) [PSYoungGen: 6635K->280K(9216K)] 6635K->6428K(19456K), 0.0025235 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 280K->0K(9216K)] [ParOldGen: 6148K->6374K(10240K)] 6428K->6374K(19456K), [Metaspace: 1615K->1615K(4400K)], 0.0063217 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 3236K [0xe6600000, 0xe7000000, 0xe7000000)
  eden space 8192K, 39% used [0xe6600000,0xe69290e8,0xe6e00000)
  from space 1024K, 0% used [0xe6e00000,0xe6e00000,0xe6f00000)
  to   space 1024K, 0% used [0xe6f00000,0xe6f00000,0xe7000000)
 ParOldGen       total 10240K, used 6374K [0xe5c00000, 0xe6600000, 0xe6600000)
  object space 10240K, 62% used [0xe5c00000,0xe62398e0,0xe6600000)
 Metaspace       used 1619K, capacity 2202K, committed 2328K, reserved 4400K

此时执行testAllocation()时分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代6635KB变为280KB,而总内存占用量则几乎没有减少(因为allocation1、allocation2、allocation3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配allocation4所需的3MB内存,因此发生Minor GC。
GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。

这次GC结束后,3MB的allocation4对象顺利分配在Eden中,因此程序执行完的结果是
Eden占用3MB(被allocation4占用),Survivor空闲,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。通过GC日志可以证实这一点。

通过对比发现,当整个新生代剩余的空间无法存放某个对象时,Parallel Scavenge/Parallel Old中该对象会直接进入老年代。而如果整个新生代剩余的空间可以存放但只是Eden区空间不足,则会尝试一次Minor GC。而对于Serial/Serial Old当发现Eden区不足以存放对象时,就进行一次Minor GC

验证大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

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

执行以下代码:

    public static void testPretenureSizeThreshold(){
        /** *VM参数:-verbose:gc-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 *-XX:PretenureSizeThreshold=3145728 *新生代采用Parallel Scavenge GC时无效 */
        byte[] allocation;
      allocation=new byte[6*_1MB]; //直接分配在老年代中
    }

通过观察GC日志会发现(新生代采用Parallel Scavenge GC时无效,-XX:PretenureSizeThreshold参数无效)。而在Serial/Serial Old收集器下该对象直接分配在老年代中。

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

public static void testTenuringThreshold(){
        /** *VM参数:-verbose:gc-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 *-XX:+PrintTenuringDistribution */
        byte[]allocation1,allocation2,allocation3;
        allocation1=new byte[_1MB/4];
        //什么时候进入老年代取决于XX:MaxTenuringThreshold设置
        allocation2=new byte[4*_1MB];
        allocation3=new byte[4*_1MB];
        allocation3=null;
        allocation3=new byte[4*_1MB];
    }

Parallel Scavenge/Parallel Old收集器下GC日志如下:

Heap
 PSYoungGen      total 9216K, used 5171K [0xe6200000, 0xe6c00000, 0xe6c00000)
  eden space 8192K, 63% used [0xe6200000,0xe670cf68,0xe6a00000)
  from space 1024K, 0% used [0xe6b00000,0xe6b00000,0xe6c00000)
  to   space 1024K, 0% used [0xe6a00000,0xe6a00000,0xe6b00000)
 ParOldGen       total 10240K, used 8192K [0xe5800000, 0xe6200000, 0xe6200000)
  object space 10240K, 80% used [0xe5800000,0xe6000020,0xe6200000)
 Metaspace       used 1650K, capacity 2202K, committed 2328K, reserved 4400K

通过观察GC日志实验发现该参数在Parallel Scavenge/Parallel Old非串行GC时无效.

对于上述代码,Serial/Serial Old收集下的GC日志为:

以MaxTenuringThreshold=1参数来运行的结果:
[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 1(max 1)
-age 1:414664 bytes,414664 total
:4859K->404K(9216K),0.0065012 secs]4859K->4500K(19456K),0.0065283 secs][Times:user=0.02 sys=0.00,real=0.02 secs]
[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 1(max 1)
:4500K->0K(9216K),0.0009253 secs]8596K->4500K(19456K),0.0009458 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4178K[0x029d0000,0x033d0000,0x033d0000)
eden 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 4500K[0x033d0000,0x03dd0000,0x03dd0000)
the space 10240K,43%used[0x033d0000,0x03835348,0x03835400,0x03dd0000)
compacting perm gen total 12288K,used 2114K[0x03dd0000,0x049d0000,0x07dd0000)
the space 12288K,17%used[0x03dd0000,0x03fe0998,0x03fe0a00,0x049d0000)
No shared spaces configured.


以MaxTenuringThreshold=15参数来运行的结果:


[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 15(max 15)
-age 1:414664 bytes,414664 total
:4859K->404K(9216K),0.0049637 secs]4859K->4500K(19456K),0.0049932 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
[GC[DefNew
Desired Survivor size 524288 bytes,new threshold 15(max 15)
-age 2:414520 bytes,414520 total
:4500K->404K(9216K),0.0008091 secs]8596K->4500K(19456K),0.0008305 secs][Times:user=0.00 sys=0.00,real=0.00 secs]
Heap
def new generation total 9216K,used 4582K[0x029d0000,0x033d0000,0x033d0000)
eden space 8192K,51%used[0x029d0000,0x02de4828,0x031d0000)
from space 1024K,39%used[0x031d0000,0x03235338,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 2114K[0x03dd0000,0x049d0000,0x07dd0000)
the space 12288K,17%used[0x03dd0000,0x03fe0998,0x03fe0a00,0x049d0000)
No shared spaces configured.

方法中的allocation1对象需要256KB内存,Survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后非常干净地变成0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代Survivor空间,这时新生代仍然有404KB被占用。

验证动态对象年龄的判定

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

public static void testTenuringThreshold2(){
        /** *VM参数:-verbose:gc-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 *-XX:+PrintTenuringDistribution */
        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];
    }

对于上述代码,Serial/Serial Old收集下的GC日志为:

[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)
eden 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.

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

验证空间分配担保

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

在JDK 6 Update 24之后,这个测试结果会有差异,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。JDK 6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

《深入理解java虚拟机 JVM高级特性与最佳实践》周志明

你可能感兴趣的:(java,jvm,GC,内存分配,parallel)