1.前言
本渣最近在学习《深入理解Java虚拟机 JVM高级特性与最佳实践》,看到关于对象内存分配的时候,书中有段测试对象分配的代码,自信满满地以为已经理解了其内存分配策略,结果自己在电脑上敲了一下,发现并不是那么回事,运行结果根本不一样,这是怎么回事呢?难道是作者大神搞错了?不,一定不是。分析本机GC日志发现,代码示例和本机默认使用的收集器不一样,代码示例中用的是 Serial/Serial Old 组合收集器,而本机的默认的是 Parallel Scavenge/Parallel Old 组合收集器,这才明白内存分配策略要根据具体使用的收集器而定。以下将针对这两种收集器组合进行详细对比。
2.环境:
java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)
JVM 为 Server 模式
3.代码
以下代码限定了Java堆大小为20M,不可扩展,其中新生代为10M,老年代为10M,且新生代中 Eden:Survivor=8:1。
/**
* 对象内存分配测试
* @author huangyan
*/
public class ObjectAllocationTest {
private static final int _1MB = 1024 * 1024;
/**
* 1.VM 参数,默认使用 Parallel Scavenge/Parallel Old:
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* 2.VM 参数,指定使用 Serial/Serial Old:
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
* VM 参数分析:
* -verbose:gc 在虚拟机发生内存回收时在输出设备显示信息,
* 格式如下: [Full GC 256K->160K(124096K), 0.0042708 secs]
* 该参数用来监视虚拟机内存回收的情况。
* -Xms20M Java堆大小最小20M
* -Xmx20M Java堆大小最大20M
* -Xmn10M 新生代10M
* -XX:+PrintGCDetails 打印GC详细信息
* -XX:SurvivorRatio=8 Dden区:Survivor区=8:1
*
* -XX:+UseSerialGC 指定使用 Serial/Serial Old 组合收集器
*/
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];
}
public static void main(String[] args) {
ObjectAllocationTest.testAllocation();
}
}
4. 默认使用 Parallel Scavenge/Parallel Old 组合收集器
(1) allocation4 大小为 4M
代码:allocation4 = new byte[4 * _1MB];
VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
运行结果:
Heap
PSYoungGen total 9216K, used 6980K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 85% used [0x00000000ff600000,0x00000000ffcd1080,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 4096K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 40% used [0x00000000fec00000,0x00000000ff000010,0x00000000ff600000)
PSPermGen total 21504K, used 2505K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
object space 21504K, 11% used [0x00000000f9a00000,0x00000000f9c72530,0x00000000faf00000)
结果分析:
代码执行到 allocation4 = new byte[4 * _1MB]; 时,前三个对象已经占用 Eden 区 6M,此时,虚拟机发现整个新生代(Eden+Survivor)已经不够分配所需的 4M(新生代共10M,Eden共8M,Eden+Survivor1=9M,9M-6M=3M<4M),因此不会发生 Minor GC,allocation4 直接进入老年代;
此时,allocation4(4M) 在 老年代,allocation1、allocation2、allocation3 (6M)在新生代 Eden 区,Survivor 区空闲。
(2) allocation4 大小为 3M
修改代码 allocation4 = new byte[4 * _1MB]; 为 allocation4 = new byte[3 * _1MB];
VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
运行结果:
[GC [PSYoungGen: 6816K->600K(9216K)] 6816K->6744K(19456K), 0.0033671 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 600K->0K(9216K)] [ParOldGen: 6144K->6613K(10240K)] 6744K->6613K(19456K) [PSPermGen: 2498K->2497K(21504K)], 0.0083242 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 9216K, used 3563K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 43% used [0x00000000ff600000,0x00000000ff97af60,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 6613K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 64% used [0x00000000fec00000,0x00000000ff275688,0x00000000ff600000)
PSPermGen total 21504K, used 2507K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
object space 21504K, 11% used [0x00000000f9a00000,0x00000000f9c72d28,0x00000000faf00000)
结果分析:
代码执行到 allocation4 = new byte[3 * _1MB]; 时,前三个对象已经占用 Eden 区 6M,此时,虚拟机发现 Eden 区所剩内存不够所需3M, 但整个新生代(Eden+Survivor)还够分配,因此会发生一次 Minor GC;
这时候虚拟机发现已有的三个对象还存活而且太大不能放入 Survivor 区,所以只好启动分配担保机制将这三个对象提前转移到老年代中;
此时,allocation4(3M) 进入 Eden区,allocation1、allocation2、allocation3(6M) 进入老年代,Survivor 区空闲。
5. 指定使用 Serial/Serial Old 组合收集器
(1) allocation4 大小为 4M
代码:allocation4 = new byte[4 * _1MB];
VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
运行结果:
[GC[DefNew: 6816K->469K(9216K), 0.0041440 secs] 6816K->6613K(19456K), 0.0041877 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 5057K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 56% used [0x00000000f9a00000, 0x00000000f9e7af60, 0x00000000fa200000)
from space 1024K, 45% used [0x00000000fa300000, 0x00000000fa375658, 0x00000000fa400000)
to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2508K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb0730b0, 0x00000000fb073200, 0x00000000fc2c0000)
No shared spaces configured.
结果分析:
代码执行到 allocation4 = new byte[4 * _1MB]; 时,因为前三个对象已经占用 Eden 区 6M,虚拟机发现 Eden 区已经不够分配 allocation4 所需的 4M,因此会发生一次 Minor GC;
这时候虚拟机发现已有的三个对象(共6M)还存活而且太大不能放入 Survivor 区(共1M),所以只好启动分配担保机制将这三个对象提前转移到老年代中;
这次 GC 完成后,allocation4 进入 Eden 区(占用4M),allocation1、allocation2、allocation3 进入老年代(占用6M)。
(2) allocation4 大小为 3M
修改代码 allocation4 = new byte[4 * _1MB]; 为 allocation4 = new byte[3 * _1MB];
VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
运行结果:
[GC[DefNew: 6816K->469K(9216K), 0.0049430 secs] 6816K->6613K(19456K), 0.0049835 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4033K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 43% used [0x00000000f9a00000, 0x00000000f9d7af60, 0x00000000fa200000)
from space 1024K, 45% used [0x00000000fa300000, 0x00000000fa375658, 0x00000000fa400000)
to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2508K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb0730b0, 0x00000000fb073200, 0x00000000fc2c0000)
No shared spaces configured.
结果分析:
运行结果同4M时类似,因为需要分配 allocation4 时,其所需的3M空间同样大于了Eden所剩的2M空间,因此还是会发生一次 Minor GC 。
这次 GC 完成后,allocation4 进入 Eden 区(占用3M),allocation1、allocation2、allocation3 进入老年代(占用6M)。
6. 总结
对于 Serial/Serial Old,是否发生 Minor GC 的判定条件是:Eden 区所剩内存是否足够分配该对象,若不够,即 Minor GC。
对于 Parallel Scavenge/Parallel Old,是否发生 Minor GC 的判定条件是:Eden 区和整个新生代所剩内存是否足够分配该对象,若够但 Eden 不够,则发生一次 Minor GC;若不够,则不会 Minor GC,该对象直接进入老年代。
最后,若不是动手实践了一番,我一定不会很快意识到不同收集器有不同的内存分配策略这一事实。所以,实践才是检验真理的唯一标准。以后的学习中一定要多动手多动手多动手。