一、序言
目前企业级主流使用的Java版本是8,垃圾回收器支持手动修改为G1,G1垃圾回收器
是Java 11的默认设置,因此G1垃圾回收器可以用很长时间,现阶段垃圾回收器优化意味着针对G1垃圾回收器优化。
为了简化讨论,下面假设针对4C/16G
物理机器进行优化。
二、G1概览
(一)了解G1
1、最大堆大小
G1管理的最大堆大小为64G。每个Region的大小通过-XX:G1HeapRegionSize
来设置,大小为1~32MB
,默认最多可以有2048个Region,G1能管理的最大堆内存是32MB*2048=64G
。
使用G1垃圾回收器最小堆内存应为1MB*2048=2GB
,低于此值建议使用其它垃圾回收器。
2、Region大小
Region大小为1~32MB
,具体取值有1MB、2MB、4MB、8MB、16MB、32MB,Region大小优化与大对象有关,当对象占用内存超过Region的一半时将被视为大对象。
被标记为大对象将不利于垃圾回收。
3、获取默认值
查看本地JVM特别是G1垃圾回收器当前的默认值。
java -XX:+PrintFlagsInitial >> ~/1.txt
(二)三种GC模式
G1垃圾回收器有两种垃圾回收模式,新生代回收
和混合回收
,特殊情况下会切换到Full GC
。
1、新生代回收
新生代回收在最大停顿时间内,会处理所有Eden区的垃圾。具体操作是将Eden区所有存活的对象复制到Survivor区,同时清空Eden区。
新生代回收伴随着应用暂停
,最长停顿时间不超过最大停顿时间
,新生代回收尽管有暂停机制,考虑到并行回收的特性,回收逻辑相对简单,回收效率依然较高。一般而言,新生代回收实际耗时通常低于最大停顿时间。
新生代回收触发时机是新创建的对象在Eden区找不到足够的存储空间。
2、混合回收
混合回收伴随着新生代回收和老年代回收,在最大停顿时间范围内,会处理大部分Eden区的垃圾和一部分老年代垃圾。
老年代回收毫无疑问会伴随着应用暂停。混合回收操作比较复杂,相对新生代回收来说,单位时间回收的垃圾数要少,回收效率要低。一般而言,混合回收的实际耗时通常接近或者等于最大停顿时间。
混合回收触发时机是由参数InitiatingHeapOccupancyPercent
控制,默认值为45,含义是老年代占用空间大小与堆的总大小比值超过此数便会触发混合回收。
默认值45%
是比较合理的,不建议所谓的调优。老年代回收策略同样是将选定Region区内存活的对象复制到空闲Region区,混合回收伴随着回收新生代垃圾能够清理出更大的空闲Region区来存放老年区存活对象,保证回收过程能够正常进行。
老年区存活对象一般较多,对象在内存中复制耗时较长,因此相对来说混合回收效率较低。
3、Full GC
Full GC是所有G1垃圾回收调优者尽力回避的情况,单线程回收垃圾,回收对象是整个堆,不再受最长停顿时间约束,一旦出现此情况,意味着应用的响应时间无情的变长。
当应用不定期进入Full GC
状态时,与其任由其单线程重塑堆内存,不如采用冗余策略,在流量低谷时刻,逐一重启应用,主动重塑堆内存空间。
流量高峰期出现Full GC现象及其应对策略后面再讨论。
(三)默认参数
1、堆内存
参数 | 默认值 | 说明 | 优化建议 |
---|---|---|---|
MaxGCPauseMillis | 200ms | 最大停顿时间 | |
G1HeapRegionSize | 不设置时启发式推断 | ||
G1NewSizePercent | 5 | 新生代最小百分比 | |
G1MaxNewSizePercent | 60 | 新生代最大百分比 |
2、新生代内存回收
参数 | 默认值 | 说明 | 优化建议 |
---|---|---|---|
ParallelGCThreads | 并行GC线程数,会根据CPU核数推断 | 默认值 | |
MaxTenuringThreshold | 15 | 从新生代晋升到老年代年龄阈值 | |
SurvivorRatio | 8 | Eden和一个Survivor的比例 | |
TargetSurvivorRatio | 50 | Survivor区内存使用率,增大该值会降低到老年代概率 | |
+G1EagerReclaimHumongousObjects | true | 是否在YGC时回收大对象 |
3、混合回收
参数 | 默认值 | 说明 | 优化建议 |
---|---|---|---|
G1MixedGCCountTarget | 8 | 值越大,收集老年代分区越少 | |
G1OldCSetRegionThresholdPercent | 10 | 表示一次最多收集10%的分区 |
三、垃圾在堆中流转
垃圾回收器调优的关键是尽可能减少Mixed GC的频率,换句话说尽可能减少垃圾流转到老年代
。GC调优便是认识垃圾在堆中的流转规律,从而对流向老年代的垃圾予以提前干涉,使之尽可能留在新生代
。
垃圾在新生代(主要指Eden区)中,垃圾回收使用YGC,回收线程与应用线程并发进行,垃圾回收对应用透明进行,假如CPU算力充足的话,应用几乎感觉不到垃圾在回收进行。
垃圾在老年代中,垃圾回收采用Mixed GC,回收线程开始工作时,应用线程阻塞,等待回收线程工作完毕有,应用线程重新被唤醒。频繁的Mixed GC对应用的吞吐量产生不良影响。
1、对象如何进入老年代
一般而言,新创建的对象会存在于新生代的Eden区,下一次垃圾回收处罚便直接回收了。如果对象比较顽强(继续被其它对象引用),那么会在Survivor区流转,每GC一次,仍然不能被垃圾回收,那么年龄加一,继续在S0和S1区流转,当年龄增长到一定的阈值,直接进入老年代。
(1)大对象直接到老年代
新创建的对象如果过大,那么不经过新生代,直接进入老年代。控制对象大小阈值有参数-XX:PretenureSizeThreshold
决定,单位字节
。
(2)动态年龄判断
除了对象在S0和S1区反复流转年龄变化外,垃圾回收维护另外一套独立的年龄判定规则:如果YGC后尚未被回收的垃圾超过了Survivor区的50%,那么超过的这批对象会直接进入老年代。
12G * 60% * 10% * 50% * 1024 = 737MB
动态年龄判定规则要求每次YGC尽可能的彻底,意味着每次GC的最长时间不能太短,默认200毫秒是比较合理的值。
如果预设置的最长停顿时间过短,那么每次GC后存活大量尚未被回收的垃圾,S区容量有限,不该进入老年代的垃圾快速在老年代堆积,频繁的Mixed GC不可避免。
2、高并发加速进入老年代
在高并发场景下,CPU和内存资源吃紧,负载很高,不确定的性能抖动加速垃圾进入老年代。
举例说明,DAO层查询数据库,一次完整的会话结束后,整个会话中产生的对象垃圾在Eden区应当被全部回收。由于网络波动,数据库处理能力的限制,大量会话超时。在此过程中这部分对象垃圾很可能在快速S0和S1流转中叠加年龄,或者触发动态年龄判定,直接进入老年代。
老年代内存空间不够用,触发Mixed GC,Mixed GC直接副作用是应用卡顿。
四、调优步骤
1、设置垃圾回收器
Java 8需要手动指定G1垃圾回收器,命令行添加-XX:+UseG1GC
参数。
2、设置堆大小
设置内存堆大小有两点需要注意:初始堆大小与最大堆大小保持一致;堆大小占物理内存大小75%~80%
,给系统核心服务预留必要的内存。
参数-Xmx12G
设置初始堆大小;参数-Xms12G
设置最大堆大小。
3、元空间设置
元空间是指存储静态类、静态方法、常量等特殊变量的内存区域。
参数-XX:MetaspaceSize=1G
设置元空间初始大小;参数-XX:MaxMetaspaceSize=1G
设置元空间最大大小。
4、GC停顿时间
GC停顿时间
是指每次YGC
或者Mixed GC
的最大时间,垃圾回收器会根据用户设置的期望时间动态选择垃圾扫描的范围,如果设置时间过小,可能总有一部分垃圾不能得到回收。单位毫秒
。
-XX:MaxGCPauseMillis=200
5、新生代大小
参数-XX:G1NewSizePercent
设置新生代初始大小,默认为5%
;参数-XX:G1MaxNewSizePercent
设置新生代最大大小,默认为60%
。
新生代内部细化为 Eden
区和两个 Survivor
,默认比例是: 8:1:1
Eden: 12G * 60%* 80% = 5.76G
S0: 12G * 60%* 10% = 0.72G
S1: 12G * 60%* 10% = 0.72G
假设并发系统每秒创建500MB的对象,假设每次YGC根据预先设置的最长停顿时间都能够扫描到Eden Region,那么此并发系统大约每隔10秒需要进行一次YGC。
五、调优实践
GC垃圾回收调优是在物理硬件受限制,并且有调优的理论空间下进行的。条件允许的话,直接升级硬件配置特别是物理内存配置,能够有效降低GC频率。比如8C32G
或者16C64G
等。
1、频繁的YGC
当并发量较大时,频繁的YGC时必然的,单位时间类创建了更多的对象,使用完毕之后成为了垃圾。频繁的YGC有加速S区对象流向老年代的可能,尽可能保证每次YGC的实际耗时低于预设置的最长垃圾回收时间(默认200毫秒),以便能够每次都能将新生代垃圾清理完成,尽可能延缓垃圾流向老年代。
2、频繁的Mixed GC
在G1垃圾回收器中,没有所谓的Mixed GC的概念,Mixed GC类似于F·GC,不同的是Mixed GC除了回收老年代,同时也回收新生代,共同之处在于都会产生STW
。
频繁的Mixed GC
本质是大量应该在新生代回收的垃圾进入了老年代,解决思路是排查哪些哪些垃圾(对象)应该留在新生代,却流转到老年代。
(1)大对象
检查应用程序是否周期性的创建大对象,大对象的阈值由参数-XX:PretenureSizeThreshold
控制。假如内存有优化空间的前提下适当调高此值,不得超过S区的一半(似乎没有这么大的对象),副作用是新生代存放对象数量相应变少,Eden区内存更快的用完,YGC相应的变频繁一些。
从业务的角度来讲,大对象产生必有其产生的原因,从这个角度优化可能性不高,垃圾回收器优化尽可能屏蔽业务层代码,毕竟对开发提要求让其不要创建大对象不现实。
(2)元空间
元空间耗尽也会引发Mixed GC
,考虑到元空间存储内容的特殊性,因元空间耗尽导致GC频率提高并没有很好的办法。单纯提高元空间大小会压缩新生代大小,新生代变小,对象流转到老年代的数量会变多,老年代内存消耗加快,同样会提高GC的频率。
因元空间耗尽引发的Mixed GC
,相对来说增加物理内存是比较优的解决方式。
3、Full GC
尽管Mixed GC
被触发时,应用会暂时停止响应(默认值是200毫秒),暂停的时间是相对可控的。
如果在进行Mixed GC
时,空闲的Region无法保存存活的对象,Mixed GC无法正常进行时,垃圾回收会切换到 G1 之外的 Serial Old GC
来收集整个堆,包括新生代、老年代、元空间等。
进入Serial Old GC
垃圾回收状态,垃圾回收不再受最长回收时间约束,采用单线程进行标记、清理和压缩整理,应用可能进入假死
状态。也许重启应用,重新分配堆内存,将堆内存彻底洗牌,也许会更好。
G1垃圾回收调优的关键是不要出现Full GC,因此对于敏感的参数千万不要乱调优,否则不仅达不到理想想过,反而更糟糕。