Young GC过程
- Young Generation
- Eden
- Survivor(Survivor1 & Survivor2)
- Old Generation
JVM将堆划分为新生代、老年代,新生代又被划分为Eden区、Survivor1区、Survivor2区(两个Survivor区相对地作为 From 和 To 逻辑区域)。
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
- Partial GC:并不收集整个GC堆的模式
- Young GC:只收集young gen的GC
- Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
- Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
- Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。
Memory Management in the Java HotSpot Virtual Machine这份白皮书中描述了垃圾回收(GC)触发的内存自动管理。
当Eden区无法再分配对象时,Young GC执行,如果JVM发现对象还十分活跃,会首先尝试将其移动到Survivor空间,而不是直接移动到老年代。
Eden区和From区中还被引用的对象会被移到To区或是晋升到老年代(两种情况下,对象会被移动到老年代)。
GC操作执行完后,Eden区与From区会被清空,在这个过程中清除了垃圾对象,也变相的完成了“压缩整理”。
From和To只是简单地表示两个Survivor空间之间的指向,每次垃圾回收时,方向都会互换,From概念上变成了To,To变成了From。
HotSpot中大部分垃圾收集器在新生代都使用copying收集算法,或者说是copying算法的变种,较大的区域作为Eden区,Survivor区起到缓冲的作用。这种布局让对象在新生代内有更多的机会被回收,不再局限于只能晋升到老年代。这就是新生代被划分成一个Eden空间和两个Survivor空间的原因。
晋升条件与相关参数
上面说到,两种情况下,对象会被移动到老年代。
- Survivor空间的大小实在太小。新生代垃圾收集时,如果目标Survivor空间被填满,Eden空间剩下的活跃对象会直接进入老年代。
- 对象在Survivor空间中经历的GC周期数有个上限,超过这个上限的对象也会被移动到老年代。这个上限值被称为晋升阈值(Tenuring Threshold)。
Survivor空间的初始大小由-XX:InitialSurvivorRatio=N
标志决定。
这个参数值在下面的这个公式中使用:
survivor_space_size = new_size / (initial_survivor_ratio + 2)
初始Survivor空间的占用比率(initial_survivor_ratio)默认为8,由此我们可以计算出每个Survivor空间会占用大约10%的新生代空间。
JVM可以增大Survivor空间的大小直到其最大上限, 这个上限可以通过-XX:MinSurvivorRatio=N
参数设置。
MinSurvivorRatio标志在下面这个公式中使用:
maximum_survivor_space_size = new_size / (min_survivor_ratio + 2)
这个参数值默认为3,意味着Survivor空间的最大值为新生代空间的20%。
JVM依据垃圾回收之后Survivor空间的占用情况判断是否需要增加或者减少Survivor空间的大小(由定义的比率决定)。
默认情况下,Survivor空间调整之后要能保证垃圾回收之后有50%的空间是空闲的。通过标志-XX:TargetSurvivorRatio=N
可以设置这个值。
最后还有一个问题,即对象在移动到老年代之前,需要在Survivor空间之间来回移动多少个GC周期。这个问题取决于晋升阈值的设定。JVM会持续地计算,寻找它认为最合适的晋升阈值。
# 设置初始的晋升阈值
# Throughput、G1默认值为7,CMS默认值为6
-XX:InitialTenuringThreshold=N
# 最大晋升阈值
# Throughput、G1默认值为15,CMS值为6
-XX:MaxTenuringThreshold=N
JVM最终会在1和最大晋升阈值之间选择一个合适的值。
避免一直“晋升”与从不“晋升”
晋升阈值总是在1到MaxTenuringThreshold之间取值。即使JVM 启动时将初始晋升阈值设置为最大值,这个参数也不一定会一直保持,JVM可能会在某个时刻减小这个阈值。
如果你确切地知道新生代垃圾收集存活下来的对象在之后很长的一段时间内都会存在,可以使用-XX:+AlwaysTenure
标志(默认值为false),启用之后对象会直接晋升到老年代,不会再存放于Survivor空间。
第二个标志是-XX:+NeverTenure
(默认值也是false)。这个标志有两方面的影响:设置参数后JVM 会认为初始晋升阈值和最大晋升阈值都无限大;一旦设置了该参数,JVM就不再调整晋升阈值,也不会将其降低。换句话说,开启标志后只要Survivor空间有容量,就不会有对象被晋升到老年代。
总结
如果Survivor空间过小,对象会直接晋升到老年代,从而可能触发更多的Full GC。
如果采用调整比率方式增大Survivor空,会导致Eden空间减少,这意味着在Minor GC(Young GC)之前能分配的对象数目会更少。
如果只是增大新生代大小,虽然对象晋升到老年代的频率降低了,但是老年代空间变得更小,应用程序可能会更频繁地发生Full GC。
如果可以增加堆的总量,那么新生代和老年代都能获得更多的内存,这是最好的解决方案。推荐的流程是增大堆的大小(或者至少增大新生代),同时减小存活率。采用这种方法Survivor空间增大的值会比Eden空间的增长更大。应用程序最终的新生代垃圾收集次数与调节之前基本持平。不过Full FC 的次数会更少,因为晋升到老年代的对象数更少了。
如果Survivor空间经过调整后不再发生溢出, 对象只有在经历的GC周期数达到MaxTenuringThreshold的设定值时才会晋升到老年代。我们可以增大MaxTenuringThreshold值,让对象在Survivor空间中停留更多的周期。但是,我们也要注意,晋升阈值增大,对象在Survivor空间停留的时间越长,将来的新生代收集中,Survivor空闲空间就会越少:越有可能发生Survivor空间溢出,对象再次被直接晋升到老年代。
引用
Java性能权威指南
https://www.zhihu.com/question/41922036/answer/93079526
http://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf