大家好,我们今天给大家介绍,Java虚拟机优先在Eden区创建对象。我们通过以下参数:-XX:+UseParallelGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails,设定一种实验场景:
Java堆大小为20MB,并且是不可扩展,其中10M分给新生代,剩下的10MB分给老年代。参数-XX:SurvivorRatio=8决定了新生代的Eden区和S0、S1存活区的空间比例是8:1:1。相等于:Eden区的大小是8M,两个Survivor区的大小都是1M。
Java虚拟机通过-XX:+PrintGCDetails日志参数打印日志。
图1-1 Java堆的分代垃圾回收策略
我们也针对这篇文章录制了一期视频:动画+源码,验证Java虚拟机优先在Eden区创建对象
图1-1的左侧是创建Java对象的流程图,右侧是在Java堆的分代垃圾管理策略。我们一起来看Java虚拟机如何优先在Eden区中创建对象。
一 对象创建过程
① 我们先创建一个大小为2M的红色对象,需要判断Eden区是否有足够的空间,此时Eden区有8M的空闲空间,按照优先在Eden区创建对象的原则,成功的在Eden区创建一个大小为2M的红色对象,如图1-2所示:
图1-2 在Eden区创建一个2M的Java对象
② 我们再创建一个大小为2M的绿色对象,此时Eden区有6M的空间,该对象创建成功。同理,再创建一个2M大小的蓝色对象,仍然可以创建成功。如图1-3所示:
图1-3 再次向Java堆中创建两个2M的Java对象
③ 我们现在要创建一个大小为4M的红色对象,此时Eden区只有2M的空间,不能存储4M的对象,需要发起一次Minor GC。如图1-4所示:
图1-4 发起一次Minor GC
④ 标记存活对象:需要标记Eden区的存活对象,如果发现这三个对象都是存活对象。因此,需要把它们复制到一个存活区(S0或者S1)中。但是每个存活区的空间只有1M,容不下任何一个存活对象。
⑤ 将存活对象晋升到老年代:因此,按照这个创建对象的流程图,就需要把这三个对象依次拷贝到老年代。然后清除掉Eden区中的所有对象。如图1-5所示:
图1-5 晋升到老年代
⑥ 在Eden区创建4M大小的新对象:此时,Eden区有8M的剩余空间,足以放得下4M的红色对象。然后将新对象创建到Eden区,如图1-6所示:
图1-6 在Eden区创建新对象
二 对象创建过程总结
图2-1 创建Java对象的流程图
- 创建对象的流程如图2-1所示,创建新对象,首先判断Eden区是否有足够的空间存放该对象;如果Eden区空间充足,直接在Eden区创建对象;否则,需要触发一次Minor GC,然后需要判断Servivor存活区是否放得下Eden区的存活对象。
- 如果S0或者S1有足够的存储空间,就把Eden区的存活对象拷贝到S0或者S1区中。每次发生Minor GC,存活对象的年龄+1,如果存活对象的年龄<15岁,就会不断的在Eden区来回复制;否则,当存活对象的年龄=15岁时,就将存活对象拷贝到老年代。
- 在我们这个示例中,因为Servivor只有1M,不能存储任何一个存活对象。就需要把存活对象拷贝到老年代。
- 然后清除Eden区的所有对象,如果此时Eden区有足够的空间,就把新对象创建到Eden区。否则,说明现在创建的是一个大对象,就把新对象创建到老年代,默认情况下老年代的空间是年轻代空间的两倍,通常情况下有足够的空间。
- 如果老年代的空间不足,就需要触发Full GC,清除老年代的垃圾对象,让老年代能腾挪出更多的空间。
- 发生Full GC之后,需要再次判断老年代,是否有足够的空间存放新对象。
- 如果有足够的空间,就分配内存,否则就只有报OOM,内存溢出了。
三 代码验证优先在Eden区创建对象
我们现在用Java代码的方式,验证Java虚拟机优先在Eden区创建对象。通过-verbose:gc -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails参数,设定Java堆大小为20MB,新生代10M,两个Survivor各为1M,Eden区8M。
public class PriorityAllocateEden {
public static void main(String[] args) {
testAllocation();
}
/**
* -verbose:gc -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* -verbose:gc 表示输出虚拟机中GC的详细情况
* -XX:+PrintGCDetails 打印GC细节
* -XX:+UseSerialGC 选择了Serial(新生代) + SeialOld(年老代)垃圾收集器组合
* -Xms 设定程序启动时占用内存大小
* -Xmx 设定程序运行期间最大可占用的内存大小
* -Xmn 是指年轻代的大小
* -XX:SurvivorRatio=8 是指Eden : Survivor大小为 8:1
*/
public static void testAllocation(){
byte alloc1[],alloc2[],alloc3[],alloc4[];
alloc1=new byte[2 * 1024*1024]; // 2M
alloc2=new byte[2 * 1024*1024]; // 2M
alloc3=new byte[2 * 1024*1024]; // 2M
alloc4=new byte[4 * 1024*1024]; // 4M 触发一次 Minor GC
}
}
通过java -verbose:gc -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails PriorityAllocateEden命令运行Java代码。
gc日志
我们先解读第一行日志:
[GC (Allocation Failure) [DefNew: 6651K->257K(9216K), 0.0691040 secs] 6651K->6401K(19456K), 0.0691840 secs] [Times: user=0.00 sys=0.07, real=0.07 secs] Heap
- GC:表明发生了一次垃圾回收,前面没有Full修饰,表明这是一次Minor GC。
- Allocation Failure:触发本次GC的原因是,年轻代的Eden区没有足够的空间存储新对象。
- DefNew:使用的是UseSerialGC,单线程垃圾收集器。
- 6651K->257K(9216K):这三个数的含义是,GC前年轻代使用的容量(6M),GC后年轻代使用的容量(257K),年轻代的总可用容量(9M),为什么不是10M,因为年轻代的可用空间=1个Survivor+Eden区=9M,另一个Survivor为不可用区(专门用来当粘贴板了)。
- 0.0691040 secs:年轻代的Minor GC耗时,单位秒。
- 6651K->6401K(19456K):整个堆回收前的大小,整个堆回收后的大小(几乎未回收到什么内存),堆的总大小。
- Times: user=0.00 sys=0.07, real=0.07 secs:分别表示用户态耗时,内核态耗时和实际耗时。
四 分析GC日志得出以下结论
在执行alloc4=new byte[4 * 1024 * 1024];时,发生了一次Minor GC,这次回收的结果是年轻代由6651K变为257K,而总内存的使用量(6651K->6401K)几乎没有减少,说明alloc1、alloc2和alloc3都是存活对象,虚拟机几乎没有找到可回收的对象。
发生这次垃圾回收的原因是,为alloc4分配内存时,发现Eden区已经使用了(6MB+257KB),剩余空间不足以分配给alloc4所需的4MB内存,因此发生Minor GC。
在标记-清除阶段又发现,存活的三个2MB大小的对象都无法copy到Survivor空间(Survivor空间只有1MB),所以只好通过分配担保机制将这些对象copy到老年代。
这次垃圾回收之后,4MB的alloc4对象顺利的分配到了Eden区,alloc1、alloc2、alloc3进入到了老年代。
通过GC日志也验证了,Java虚拟机优先在Eden区创建对象,为什么要优先在Eden区创建对象呢?因为Eden区垃圾回收的效率比老年代要高很多。