Java GC 调优

记录Java GC调优的相关问题。

常用

常用的方式如下,

第一种:ParNew(新生代)CMS(老年代)

第二种:G1(新生轻与老年代)

分类

按照GC范围的不同,GC可分为:MinorGC(YoungGC)MajorGC(FullGC)OldGCMixedGC

MinorGC

MinorGC新生代GC,在分代回收中负责堆内存年轻代的回收。

触发条件

Eden区空间不足就会触发MinorGC

在分代回收中,如果年轻代的空间不足会导致年轻代的垃圾回收。

回收算法

由于新生代的对象生命周期较短,垃圾回收也会相对频繁,因此需要考虑垃圾回收的效率。

考虑到新生代朝生夕死的特点,新生代采用复制算法,例如,ParNew中仅复制存活数据到Survivor

具体步骤

① 扫描新生代以及老年代;

由于存在跨代引用,因此需要扫描老年代。

CMS使用CardTable来记录新老代之间的跨代引用,
G1使用RememberSet(占堆的20%)来记录Region之间的跨代引用。

② 复制存活对象到Survivor区,增加对象年龄;

复制属于耗时操作,但由于Eden区对象朝生夕死的特点,需要复制的对象并不多。。

③ 年龄达到阈值,晋升至老年代。

注意:单次MinorGC时间更多取决于GC后存活对象的数量

OldGC

OldGC老年代GC

CMS属于OldGC,仅对分代回收中的老年代进行老几回收,一般配合新生代ParNew

触发条件

① 手动触发System.gc()

② 老年代的使用率达到阈值;

回收算法

由于老年代的对象生命周期较长,存活的对象比例高,因此无法使用复制算法。

CMS采用标记清除算法,同样可以开启内存整理功能来避免内存空间碎片的问题。

标记清除中常见的问题:STW(Stop The World)

STW(Stop The World)是为了保证标记的一致性,避免标记过程中出现遗漏的情况。

STW并不是必须的,但是在极端情况下,遗漏可能会导致大量的内存泄露,甚至导致宕机。

具体步骤

Initial Mark:从GC Root开始标记可达对象,触发STW

什么是GC Root

Local variables are kept alive by the stack of a thread.
Active Java threads are always considered live objects and are therefore GC roots.
Static variables are referenced by their classes.
JNI References are Java objects that the native code has created as part of a JNI call.

Cocurrent Mark:根据①中标记的可达对象并发遍历标记相关联的对象状态;

Remark:重新标记在并发标记阶段发生变化的对象,触发STW

Concurrent Sweep:在对象标记的基础之上,清理老年代非可达对象。

MajorGC

MajorGCFullGC,不仅会触发OldGC,也会触发YoungGC

MajorGC是收集整个堆内存。

MajorGC会自动触发MinorGC

MixedGC

MixedGC新生代与老年代的混合GC,它不属于FullGC

MixedGC是建立在YoungGC的基础之上再回收部分老年代的内存

G1属于MixedGC

G1通过划分Region来实现增量回收。

触发条件

E区无法分配新的对象内存时会触发G1中的YoungGC

使用率大于InitiatingHeapOccupancyPercent会触发MixedGC

Metaspace内存不足时会触发MixedGC

回收算法

YongGC选定所有年轻代Region进行回收,使用复制算法

MixedGC不仅选定所有年轻代Region,还会根据global concurrent marking统计得出收集后收益高的若干老年代Region来选择,最终采用标记清除算法

G1保证在用户指定的开销目标范围内尽可能选择收益高的老年代Region进行回收。

因此,MixedGC不是FullGC

MixedGC无法解决老年代内存不足时,会降级为SerialGC(FullGC)

具体步骤

Initial Mark:同CMS的步骤①;

Cocurrent Mark:同CMS的步骤②;

SATB是GC开始时活着的对象的一个快照(通过Root Tracing得到),用于维持并发GC的正确性。

Remark:与CMS的区别在于重新标记的范围不同:G1仅需要扫描SATB(snapshot-at-the-beginning, 起始快照)

CMS Remark的扫描范围不仅包括SATB,而且会扫描整个根集合。

Clean up/Copy:在对象标记的基础之上,清理新生代全部非可达对象与老年代部分非可达对象。

FullGC

FullGC产生的原因有以下四种,

MetaSpcae空间不足与自动扩容

在Java8中,当MetaSpace空间内存不足时,会触发FullGC来尝试清理掉无用的内存。

为了避免这种情况,可以通过增大MetaSpace空间大小预设足够大的MetaSpace空间避免动态扩容

CMS的promotion failed与concurrent mode failure

MinorGC时,如果Survivor空间不足,对象会直接进入老年代,

但由于老年代有碎片或者剩余空间不足导致没有足够空间存储晋升对象,就会产生promotion failed

promotion failed会导致GC降级为SerialGC(Old)

解决方法:

① 增大Survivor大小;

内存整理属于耗时操作,会造成STW

② 设置老年代的内存碎片整理功能以及合理的整理周期


CMS GC时,如果由于某种原因,业务线程直接在老年代内分配对象,但老年代没有足够的空间,就会产生concurrent mode failure

同样,concurrent mode failure会导致GC降级为SerialGC(Old)

解决方法:

增大老年代大小,避免老年代的内存不足;

② 设置老年代的合理回收阈值,尽早释放内存空间,保证老年代的剩余空间大小;

Young GC晋升的平均大小大于老年代的剩余空间

老年代的内存空间存在内存不足的风险时,会触发FullGC,如果频繁出现此类情况,需要关注晋升对象大小以及是否存在内存泄露的情况。

主动触发Full GC(System.gc)

应用通过调用System.gc()来触发FullGC

配置调优

基础

-Xms & -Xmx

-Xms: 初始堆大小。

-Xmx: 最大堆大小。

一般情况下设置为相同值,避免内存扩展。

-Xmn

-Xmn: 新生代大小。

Sun官方推荐配置为整个堆的3/8,但是不同的业务场景应该不同。

对于Web应用3/8设置并不合理,原因在于:
每个请求的生命周期较小,尤其对于高并发的场景下,大量的并发会导致新生代快速填满;
由于新生代内存不足,请求对象直接进入老年代,这部分对象并不会被MinorGC清理,从而造成内存空间的浪费。

① 增大Eden增大触发间隔:

  • [Eden 2] 会影响 [Minor GC间隔 2]:
  • 内存空间增大一倍,空间被占满的时间也会同步增大一倍。

② 增大Eden对单次MinorGC时间的影响不大:

  • [Eden * 2] 会增加新生代扫描与复制的时间;
  • 扫描时间占比很小(并发扫描),对MinorGC影响不大;

复制是一个耗时的操作,但Eden区由于对象的生命周期较小,需要复制的对象也不会增加太多。

  • 由于堆中短期对象很多,不需要额外复制一倍的空间;

因此,增加Eden区大小可以提高新生代内存回收(MinorGC)的效率

-XX:MetaspaceSize & -XX:MaxMetaspaceSize

该用于设置元空间的大小,存在内存不足自动扩容的情况,可能造成MajorGC

64位JVM默认20M,最大值为宿主机内存大小。

CMS

-CMSScavengeBeforeRemark

该配置时用来保证Remark前强制进行一次MinorGC,从而减少Remark的时间

考虑到新生代对象的生命周期很短,在触发CMS之前强制JVM执行一次Minor GC,清理掉无效的对象,避免大范围的Remark

-XX:UseCMSCompactAtFullCollection & -XX:CMSFullGCBeforeCompaction

该配置是用于解决promotion failed

分别为开启开启CMS GC的内存整理功能设置CMS GC的内存整理频次

从而减少内存碎片造成的内存不足。

-XX:CMSInitiatingOccupancyFraction & -XX:+UseCMSInitiatingOccupancyOnly

该配置是用于解决cocurrent mode failed

分别为设定CMS在对内存占用率达到X%的时候开始GC设置JVM回收阈值(不基于运行时收集的数据来启动CMS垃圾收集周期)

从而避免垃圾回收不及时造成的内存不足。

G1

-XX:NewRatio

该配置是用来设置新生代与老年代的比例(默认2)。

调整新生代的大小可避免大量短生命周期对象进入老年代。

-XX:MaxGCPauseMillis=n

该配置是用来设置最大GC停顿时间的目标,主要是为了降低STW对应用的影响

虽然设置了最大GC停顿时间,但这只是JVM自动回收优化的目标,不保证每次都会低于该配置的值。

-XX:G1ReservePercent

该配置是用来设置堆内存保留空间的大小,用以降低Eden晋升失败的可能性。

当对内存空间大小达到堆内存预留值会触发MixedGC。

-XX:G1HeapRegionSize

该配置是用来设置堆Region的大小,默认1~32。

在特殊的业务场景下可能存在小生命周期的大对象的产生,可以通过此设置优化Region大小解决大对象直接进入老年代的问题

美团的案例

参考

美团的案例
https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/
https://hllvm-group.iteye.com/group/topic/44381#post-272188