之前的两篇文章(java运行时数据区浅析、java垃圾收集器(GC)浅析)介绍了java运行时数据区以及GC垃圾回收算法的相关知识,大家可以先去看看这两篇文章。
本篇文章将重点关注新生代的垃圾回收。
大部分JVM都会采用所谓的分代收集方式去回收垃圾,什么是分代收集呢?
根据对象的存活周期的不同将内存划分为好几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,那就选用复制算法。老年代因为对象存活率高,没有额外空间对它进行分配担保,那就必须使用“标记-清理”或者“标记-整理”等重量级算法来进行回收。
单纯从JVM的功能考虑,并不需要新生代,完全可以针对整个堆进行操作。新生代存在的唯一理由是优化垃圾回收的性能。更具体的说,把堆划分为新生代和老年代有两个好处:
1.简化了新对象的分配(只在新生代分配内存);
2.可以更有效的清除不再需要的对象(即死对象,新生代和老年代使用不同的回收算法)。
通过广泛研究面向对象实现的应用,发现一些特点:
1.大部分对象的生存时间都很短;
2.新生对象很少引用生存时间长的对象。
所以,新生代通过复制算法可以高效的回收垃圾。
根据上面的思想,我们继续将堆区域划分成如下部分:
首先堆可以划分为新生代和老年代,然后新生代又可以划分为一个Elden区和两个Survivor(幸存)区。
按照规定,新对象会首先分配在Eden中(如果对象过大,比如大数组,将会直接放到老年代)。在GC中,Eden中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过minor GC的次数),会被移动到老年代。
新生代采取复制算法,在Minor GC之前,to survivor区域保持清空,对象保存在Eden和from survivor区,minor GC运行时,Eden中的幸存对象会被复制到to Survivor(同时对象年龄会增加1)。而from survivor区中的幸存对象会考虑对象年龄,如果年龄没达到阈值,对象依然复制到to survivor中。如果对象达到阈值那么将被移到老年代。复制阶段完成后,Eden和From幸存区中只保存死对象,可以视为清空。如果在复制过程中to幸存区被填满了,剩余的对象将被放到老年代。最后,From
survivor和to survivor会调换一下名字,下次Minor GC时,To survivor变为From Survivor。
过程如下图所示:
总结一下,对象一般出生在Eden区,年轻代GC过程中,对象在2个幸存区之间移动,如果对象活到适当的年龄,会被移到老年代。当对象在老年代中死亡时,就需要更高级别的GC,更重量级的GC算法。
调优的重要性:新生代的大小设置非常重要,如果新生代过小,会导致新生对象很快就晋升到老年代中,在老年代中对象很难被回收。如果新生代过大,会发生过多的复制过程。因而我们需要找到一个合适的大小,不幸的是,要想获得一个合适的大小,只能通过不断的测试调优,这就需要JVM参数了。
新生代垃圾回收