一篇文章拆解Java主流垃圾回收器及其调优方法。
本文将深入且核心的Java性能优化话题。详细拆解Java主流垃圾回收器及其调优方法。
一、 图解概览:垃圾回收器与算法关系
首先,通过下图可以快速了解各垃圾回收器、其使用的算法以及它们之间的组合关系:
二、 各垃圾回收器详解与调优
1. Serial / Serial Old
- 算法:
- 年轻代 (Serial):复制算法 (Copying)。将Eden和Survivor From中存活的对象复制到Survivor To区,然后清空Eden和From。
- 老年代 (Serial Old):标记-整理算法 (Mark-Compact)。标记存活对象,然后将它们向内存一端移动,清理掉边界以外的内存。
- 特点:单线程,GC时会暂停所有用户线程(Stop-The-World)。
- 适用场景:Client模式下的JVM;内存资源受限的嵌入式系统。
- 调优:通常无需特意调优。若使用,关注点与其他收集器类似(堆大小、代大小比例)。
2. ParNew
- 算法:复制算法 (Copying)(同Serial)。
- 特点:Serial收集器的多线程并行版本,是CMS收集器在年轻代的默认搭档。
- 适用场景:与CMS搭配使用,用于服务端应用的年轻代收集。
- 调优参数:
-XX:ParallelGCThreads=N
:设置用于年轻代GC的并行线程数。通常设置为等于或略小于CPU核心数。
3. Parallel Scavenge / Parallel Old (JDK 8默认)
- 算法:
- 年轻代 (Parallel Scavenge):复制算法 (Copying)。
- 老年代 (Parallel Old):标记-整理算法 (Mark-Compact)。
- 特点:并行的、多线程的,但其目标是达到一个可控制的吞吐量(Throughput)(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间))。
- 适用场景:后台运算、科学计算、批处理任务,不太关心单个请求的延迟。
- 调优参数:
-XX:MaxGCPauseMillis=N
:设置期望的最大GC停顿时间(毫秒)。JVM会尽力但不保证实现。调小此值可能会增加GC频率,反而降低吞吐量。-XX:GCTimeRatio=N
:设置吞吐量目标(0-100)。值是比率,例如19表示GC时间占总时间的1/(1+19)=5%。默认99,即1%的时间用于GC。-XX:+UseAdaptiveSizePolicy
(默认开启):开启后,JVM会动态调整年轻代大小(-Xmn)、Eden与Survivor比例(-XX:SurvivorRatio)、晋升老年代年龄阈值(-XX:MaxTenuringThreshold)等参数以接近设定的停顿时间或吞吐量目标。这是Parallel系收集器的一大优势,调优时通常只需设置最大堆(-Xmx)和性能目标(-MaxGCPauseMillis/-GCTimeRatio),细节由JVM自适应完成。
4. CMS (Concurrent Mark Sweep)
- 算法:标记-清除算法 (Mark-Sweep)。但为了弥补标记-清除产生的碎片,提供了参数
-XX:+UseCMSCompactAtFullCollection
(默认开启)在Full GC时进行碎片整理(Mark-Compact)。 - 特点:以获取最短回收停顿时间为目标,大部分GC工作(标记阶段)与用户线程并发执行。
- 适用场景:Web服务器、B/S系统前端,重视服务的响应速度。
- 调优参数:
-XX:CMSInitiatingOccupancyFraction=N
:至关重要。设置老年代空间使用率达到多少百分比时触发CMS收集(JDK5默认68,JDK6+默认92)。不宜过高或过低。过高可能导致“并发模式失败”(Concurrent Mode Failure),触发Serial Old;过低则GC过于频繁。-XX:+UseCMSInitiatingOccupancyOnly
:强制JVM始终使用CMSInitiatingOccupancyFraction
的值作为触发条件,而不是动态调整。-XX:ConcGCThreads
/-XX:ParallelGCThreads
:设置并发标记和并发整理的线程数。通常为ParallelGCThreads的1/4左右。- 应对并发模式失败:如果日志中出现
Concurrent Mode Failure
,说明CMS跟不上对象分配/晋升的速度。解决方案:1) 降低CMSInitiatingOccupancyFraction
触发百分比;2) 增加老年代大小(即整个堆大小);3) 增加后台GC线程数;4) 或者换用G1收集器。
5. G1 (Garbage-First) (JDK9+默认)
- 算法:整体看是标记-整理(Mark-Compact),局部(两个Region之间)是复制算法(Copying)。
- 特点:将堆划分为多个大小相等的Region,优先回收垃圾最多的区域(Garbage-First名的由来),同时能预测停顿时间。
- 适用场景:面向服务端,大内存(>4G)、多核心机器,追求低延迟且吞吐量也不错的通用型收集器。
- 调优参数:
-XX:MaxGCPauseMillis=N
:设置期望的最大停顿时间目标(如200ms)。这是G1的核心调优参数,G1会尽力通过调整每次回收的Region数量来达成目标。-XX:InitiatingHeapOccupancyPercent=N
(IHOP):设置整个堆使用率达到多少时触发并发标记周期(默认45%)。这是G1的“老年代”填充触发机制。如果Mixed GC来不及回收,导致Full GC,可以调低此值,让G1更早开始标记。-XX:ConcGCThreads=N
:设置并发标记阶段的线程数。- Region大小:使用
-XX:G1HeapRegionSize=M
设置,范围1M-32M,必须是2的幂。G1会自动计算,通常无需手动指定。
6. ZGC & Shenandoah
- 算法:并非传统分代算法,而是基于Region的、几乎全阶段并发的革命性算法。ZGC使用着色指针(Colored Pointers)和读屏障(Load Barrier) 来实现并发整理。
- 特点:超低停顿(通常<10ms,且几乎不随堆大小增长而增加),高吞吐量损耗。
- 适用场景:超大堆(TB级别)且对延迟极度敏感的应用(如金融交易、实时系统)。
- 调优:目前调优目标相对简单,主要是设定最大停顿时间。
-XX:MaxGCPauseMillis=N
:同样用于设定停顿时间目标。- 分配速率:这类收集器的性能瓶颈往往在于对象的分配速率(Allocation Rate)。如果分配太快,GC跟不上,也会触发STW。优化代码,减少对象分配是根本。
三、 通用调优思路与步骤
- 明确目标:是追求高吞吐量(Parallel Scavenge/Old)还是低延迟(CMS, G1, ZGC)?这是选择收集器的首要依据。
- 监控先行:没有监控就没有调优。使用
jstat
,gcviewer
,GCeasy
等工具分析GC日志,关注:- YoungGC/FulllGC频率和耗时
- 吞吐量和停顿时间
- 老年代内存变化趋势
- 是否出现
Concurrent Mode Failure
或Promotion Failed
等错误。
- 设置堆大小:
-Xms
和-Xmx
设为相同值,避免堆震荡。 - 选择收集器:根据目标和监控结果选择。
- JDK8:吞吐量选
-XX:+UseParallelGC
,低延迟选-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
。 - JDK11+:优先选择G1 (
-XX:+UseG1GC
),超大堆或极致延迟选ZGC (-XX:+UseZGC
)。
- JDK8:吞吐量选
- 精细化调优:根据收集器特性设置上述提到的关键参数(如CMS的
CMSInitiatingOccupancyFraction
,G1的MaxGCPauseMillis
)。 - 检查代码:很多时候GC问题的根源是代码问题,如内存泄漏、过度分配、不必要的对象创建等。JVM调优是最后的手段,而非首选。
希望这份详细的总结能帮助读者更好地理解和调优Java垃圾回收器!