JVM调优
堆空间如何设置
在分代模型中,各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。
活跃数据的大小:应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。
可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。
扩容新生代能提高GC效率吗?
通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此可以通过增大新生代空间来降低Minor GC的频率。例如在相同的内存分配率的前提下,新生代中的Eden区增加一倍,Minor GC的次数就会减少一半。扩容Eden区虽然可以减少Minor GC的次数,但会增加单次Minor GC时间啊,单次时间增加了,是不是也白忙活了?
单次Minor GC时间由以下两部分组成:T1(扫描新生代)和 T2(复制存活对象到Survivor区)如下图:
扩容前:新生代容量为R ,假设对象A的存活时间为750ms,Minor GC间隔500ms,那么本次Minor GC时间= T1(扫描新生代R)+T2(复制对象A到S)。
扩容后:新生代容量为2R ,对象A的生命周期为750ms,那么Minor GC间隔增加为1000ms,此时Minor GC对象A已不再存活,不需要把它复制到Survivor区,那么本次GC时间 = 2 × T1(扫描新生代R),没有T2复制时间。
可见,扩容后,Minor GC时增加了T1(扫描时间),但省去T2(复制对象)的时间,更重要的是对于虚拟机来说,复制对象的成本要远高于扫描成本,所以,单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小。
所以当JVM服务中存在大量短期临时对象,扩容新生代空间后,Minor GC频率降低,对象在新生代得到充分回收,只有生命周期长的对象才进入老年代。这样老年代增速变慢,Major GC频率自然也会降低。
但是如果堆中短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加。
总结的经验就是
:如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。
JVM是如何避免Minor GC时扫描全堆的?
新生代GC和老年代的GC是各自分开独立进行的。
新生代对象持有老年代中对象的引用,老年代也可能持有新生代对象引用,这种情况称为“跨代引用”。
因它的存在,所以Minor GC时也必须扫描老年代。
JVM是如何避免Minor GC时扫描全堆的?
经过统计信息显示,老年代持有新生代对象引用的情况不足1%,根据这一特性JVM引入了卡表(card table)来实现这一目的。
卡表
的具体策略是将老年代的空间分成大小为512B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。
如上图所示,卡表3被标记为脏,之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。