引言:为什么垃圾回收如此重要?
在Java的世界里,"自动内存管理"是吸引无数开发者的核心特性之一------开发者无需手动分配和释放内存,这一重任交给了Java虚拟机(JVM)的垃圾回收器(Garbage Collector,GC)。然而,"自动"不代表"无感知":垃圾回收过程中可能出现的**"Stop The World"(STW,所有用户线程暂停)**停顿,会直接影响应用的性能和用户体验。
想象这样的场景:你正在使用的在线支付系统,在处理交易时突然卡顿几百毫秒;或者你玩的手游,在团战关键时刻突然掉帧------这很可能是JVM垃圾回收在"背后工作"导致的。因此,深入理解JVM垃圾回收器的工作原理、流程及适用场景,是每一位Java工程师进阶的必经之路。
第一章:JVM垃圾回收的基础认知
1.1 垃圾的定义:什么样的对象需要回收?
垃圾回收的核心是"识别并回收不再被使用的对象"。JVM采用两种核心算法判定"垃圾":
(1)引用计数法
给每个对象维护一个引用计数器,被引用一次则计数+1,引用失效则-1。当计数器为0时,对象可回收。
缺点 :无法解决循环引用问题(如对象A引用B,B引用A,且两者无其他外部引用,此时计数器均不为0,但实际应被回收)。
(2)可达性分析(Root Tracing)
JVM的主流选择。通过一系列**"GC Roots"**作为起点,遍历对象引用图,不可达的对象即为垃圾。
GC Roots包括:
- 虚拟机栈中局部变量表的引用;
- 方法区中类静态属性的引用;
- 方法区中常量的引用;
- 本地方法栈中JNI(Native方法)的引用等。
1.2 分代假设:为什么要"分代回收"?
JVM将堆内存划分为年轻代(Young Generation) 、老年代(Old Generation) ,以及元空间(MetaSpace,替代永久代,存储类元数据)。这种划分基于"分代假设":
- 大部分对象"朝生夕死"(创建后很快失效);
- 存活过多次垃圾回收的对象,更可能长期存活。
年轻代又分为Eden区 和两个Survivor区(From、To) ,比例通常为8:1:1
。对象先在Eden创建,经历几次回收后仍存活的,会被移到老年代。
第二章:串行垃圾收集器(Serial)

2.1 Serial收集器的核心特点
Serial是最基础的垃圾回收器,单线程 工作(同一时间只有一个GC线程执行),且在回收时会触发STW(所有用户线程暂停)。
- 适用场景:客户端模式(如桌面应用)、内存较小的应用。因为单线程回收时,没有线程切换开销,在小堆上效率很高。
2.2 年轻代Serial收集器的回收流程
当Eden区被对象填满时,Serial收集器触发Young GC,流程如下:
- STW暂停用户线程:确保回收过程中对象引用不发生变化。
- 根节点枚举:从GC Roots出发,标记所有可达对象。
- 复制算法(Copying)回收Eden和Survivor :
- 将Eden和
From Survivor
中存活的对象 ,复制到To Survivor
区; - 若
To Survivor
空间不足,或对象年龄达到阈值(默认15,可通过-XX:MaxTenuringThreshold
调整),则将对象晋升到老年代。
- 将Eden和
- 清空Eden和From Survivor:回收这些区域的内存,供新对象分配。
2.3 老年代Serial Old收集器的回收流程
Serial Old是Serial收集器的老年代版本,采用标记-整理算法(Mark-Compact),流程为:
- STW暂停用户线程。
- 标记阶段:从GC Roots出发,标记所有存活对象。
- 整理阶段 :将所有存活对象向一端移动,使内存形成连续的空闲区域。
- 清除阶段:回收移动后剩下的"空闲尾部"内存。
2.4 Serial收集器的优缺点
- 优点:实现简单,单线程下无竞争,CPU开销小,在小堆上停顿时间短(几十毫秒内)。
- 缺点 :STW停顿时间随堆大小增加而线性增长,大堆场景下无法接受(如堆内存为几GB时,停顿可能达数百毫秒)。
第三章:并行垃圾收集器(Parallel)

3.1 并行收集器的设计目标:吞吐量优先
Parallel收集器是Serial的多线程版本 ,核心追求是高吞吐量 (公式:用户代码执行时间 / (用户代码执行时间 + GC时间)
)。适合服务器端批量处理场景(如后台计算、大数据分析)。
Parallel家族包括:
- Parallel Scavenge:年轻代收集器,多线程执行;
- Parallel Old:老年代收集器,JDK 6后推出,支持多线程"标记-整理"。
3.2 年轻代Parallel Scavenge的回收流程
与Serial年轻代流程类似,但多线程并行回收:
- STW暂停用户线程。
- 多线程根节点枚举与标记:多个GC线程同时遍历引用图,标记存活对象。
- 多线程复制回收 :多个线程同时将Eden和
From Survivor
的存活对象复制到To Survivor
或晋升到老年代。 - 清空回收区域。
JVM提供参数-XX:ParallelGCThreads
控制GC线程数量(默认与CPU核心数相关)。
3.3 老年代Parallel Old的回收流程
Parallel Old采用多线程标记-整理算法,流程为:
- STW暂停用户线程。
- 多线程标记:多个线程同时标记存活对象。
- 多线程整理:多个线程协同将存活对象向一端移动,压缩内存。
- 清除空闲内存。
3.4 并行收集器的优缺点
- 优点:多线程并行回收,大幅提升吞吐量;在大堆场景下,停顿时间比Serial短(但仍随堆大小增加而增加)。
- 缺点:STW停顿依然存在,且为了追求吞吐量,对停顿时间的控制不如后续的CMS、G1灵活。
第四章:CMS(Concurrent Mark Sweep)收集器:低停顿的尝试

4.1 CMS的设计目标:低延迟优先
CMS(Concurrent Mark Sweep)是第一款真正意义上的并发垃圾收集器 ------它试图让垃圾回收与用户线程尽可能并行执行 ,从而减少STW停顿时间。适合延迟敏感型应用(如Web服务、实时系统)。
4.2 CMS的回收流程:七阶段详解
CMS的核心流程分为七个阶段,其中初始标记、重新标记、并发重置需要STW,其余阶段与用户线程并行:
阶段1:初始标记(Initial Mark)
- STW,时间极短。
- 标记GC Roots直接关联的对象(如根引用直接指向的对象)。
阶段2:并发标记(Concurrent Mark)
- 与用户线程并行执行。
- 从初始标记的对象出发,遍历整个对象图,标记所有可达对象。
- 此时用户线程仍在运行,可能导致对象引用变化,产生"浮动垃圾"(并发标记后产生的垃圾,需下次回收)。
阶段3:预清理(Concurrent Preclean)
- 与用户线程并行执行。
- 处理"并发标记"期间,因用户线程操作导致的引用变化(如对象新增、引用关系改变),减少后续"重新标记"的工作量。
阶段4:可取消的预清理(Concurrent Abortable Preclean)
- 与用户线程并行执行。
- 尝试消耗CPU空闲时间,进一步处理引用变化,直到满足"超时时间"或"清理进度阈值"。
阶段5:重新标记(Final Remark)
- STW,时间比初始标记长,但远短于全堆标记。
- 修正"并发标记"和"预清理"期间因用户线程操作导致的标记误差,确保标记结果准确。
- 会使用"卡表(Card Table) "和"写屏障(Write Barrier)"记录引用变化,辅助修正。
阶段6:并发清除(Concurrent Sweep)
- 与用户线程并行执行。
- 遍历堆内存,清除所有未被标记的对象(采用"标记-清除"算法,不移动对象)。
阶段7:并发重置(Concurrent Reset)
- 与用户线程并行执行。
- 重置CMS内部数据结构(如卡表、标记位图),为下一次回收做准备。
4.3 CMS的优缺点与问题
优点:
- 大部分阶段与用户线程并行,STW停顿时间短(初始标记和重新标记的停顿通常在几十毫秒级别)。
- 适合延迟敏感场景,能有效提升应用响应性。
缺点与挑战:
- CPU资源敏感:并发阶段需要占用CPU资源,若CPU核心数少,会与用户线程争夺资源,导致应用吞吐量下降。
- 内存碎片问题 :采用"标记-清除"算法,回收后内存会产生碎片。当老年代碎片过多时,大对象无法分配,会提前触发Full GC(通常用Serial Old执行,停顿时间长)。
- 浮动垃圾问题 :并发清除阶段产生的新垃圾无法回收,只能等到下次GC,因此需要预留足够内存供用户线程运行,否则会触发"Concurrent Mode Failure"(并发回收时内存不足,转而执行Serial Old的Full GC)。
- 老年代回收阈值难调 :CMS通过
-XX:CMSInitiatingOccupancyFraction
设置老年代触发回收的阈值(默认68%),若设置过高,易触发Concurrent Mode Failure;设置过低,会增加GC频率。
第五章:G1垃圾收集器:区域化与可预测停顿的革命
5.1 G1的核心创新:区域化分代+停顿预测
G1(Garbage-First)是JDK 7引入的新一代垃圾收集器,旨在兼顾吞吐量与延迟 ,并支持可预测的停顿时间。它的核心设计包括:
- 区域化堆内存 :将堆划分为多个大小相等的Region (通常1MB~32MB,可通过
-XX:G1HeapRegionSize
调整)。每个Region属于年轻代(Eden/Survivor)或老年代,但角色可动态变化。 - 优先回收"垃圾多"的Region:通过跟踪每个Region的垃圾占比,优先回收垃圾多的Region("Garbage-First"的由来),从而高效利用回收时间。
- 停顿预测模型 :通过
-XX:MaxGCPauseMillis
(默认200ms)设置目标停顿时间,G1会根据历史回收数据,动态选择要回收的Region数量,确保停顿不超过目标。
5.2 Young Collection:年轻代垃圾回收
当Eden区的Region被填满时,G1触发Young GC,流程如下:
- STW暂停用户线程。
- 扫描根(Root Scanning):从GC Roots出发,标记年轻代内的可达对象。
- 更新Remembered Set(RSet) :G1通过RSet 解决跨Region引用问题------每个Region维护一个RSet,记录"其他Region中指向本Region的引用"。回收时,只需扫描RSet即可找到外部引用,无需全堆扫描。
- 复制存活对象:将Eden和Survivor Region中存活的对象,复制到新的Survivor Region或老年代Region(若年龄达标)。
- 回收Region:清空被回收的Eden和Survivor Region,标记为"空闲",供新对象分配。



5.3 Young Collection + Concurrent Mark:并发标记周期
当老年代占用率达到一定阈值,或Young GC频繁时,G1会启动并发标记周期,流程为:
阶段1:初始标记(Initial Mark)
- 伴随一次Young GC执行(STW),标记年轻代指向老年代的根对象。
阶段2:并发标记(Concurrent Mark)
- 与用户线程并行执行,遍历对象图,标记所有可达对象。
阶段3:最终标记(Final Mark)
- STW,处理并发标记期间的"写屏障"记录(如对象引用变化),确保标记准确。
阶段4:筛选回收(Live Data Counting and Evacuation Preparation)
- 统计每个Region的存活对象数量和垃圾占比,为后续"混合回收"选择要回收的Region。


5.4 Mixed Collection:混合垃圾回收
并发标记完成后,G1触发Mixed GC ,同时回收年轻代Region和部分老年代Region,流程如下:
- STW暂停用户线程。
- 选择回收的Region:根据停顿预测模型,选择一批"垃圾多、回收收益高"的Region(包括年轻代和老年代)。
- 复制存活对象:多线程将选中Region中的存活对象,复制到新的空闲Region(可能是年轻代或老年代)。
- 回收Region:清空被选中的Region,标记为空闲。
Mixed GC会执行多次,逐步回收老年代的Region,直到老年代占用率低于阈值。

5.5 G1的优缺点与适用场景
优点:
- 可预测的停顿时间:通过停顿预测模型和区域化回收,能有效控制GC停顿在目标时间内。
- 减少内存碎片:采用"复制算法"回收Region,存活对象被移动到新Region,天然避免碎片。
- 灵活的堆内存管理:Region角色动态变化,无需严格区分年轻代和老年代的物理边界。
- 兼顾吞吐量与延迟:相比CMS,内存碎片问题得到解决;相比Parallel,停顿时间更可控。
缺点:
- Region管理开销:维护RSet、Region角色切换等带来额外CPU和内存开销,小堆(如小于4GB)场景下,效率可能不如Parallel收集器。
- 复制算法的开销:Mixed GC时复制存活对象需要额外内存空间,若堆内存紧张,可能影响回收效率。
适用场景:
- 堆内存较大(建议8GB以上)的服务器端应用。
- 延迟敏感且希望停顿可预测的应用(如微服务、电商系统)。
第六章:现代垃圾回收器:ZGC与Shenandoah
JDK的垃圾回收器仍在演进,以ZGC和Shenandoah为代表的新一代收集器,瞄准**"亚毫秒级停顿"和"超大内存(TB级)"**场景。
6.1 ZGC:基于染色指针的低延迟收集器
ZGC(Z Garbage Collector)是JDK 11引入的实验性收集器,JDK 15后转正。它的核心特点:
- 染色指针(Colored Pointers):通过在指针的高几位存储标记信息(如"是否被标记""是否被移动"),避免了传统"标记-清除"或"复制"时的额外操作。
- 读屏障(Read Barrier) :在加载对象引用时插入屏障,处理对象移动后的指针修正(ZGC支持并发移动对象,用户线程访问时由读屏障保证指针有效性)。
- 全并发回收:几乎所有阶段(标记、移动、回收)都与用户线程并行,STW停顿仅存在于"根节点枚举"等极短阶段(通常在10毫秒以内,甚至亚毫秒级)。
ZGC适合超大内存、低延迟的场景(如大型分布式系统、实时数据库)。
6.2 Shenandoah:基于读屏障与并发移动的收集器
Shenandoah与ZGC设计思路相似,但实现细节不同:
- 并发移动与读屏障:同样支持并发移动对象,通过读屏障保证指针正确性。
- 内存回收流程:包括初始标记、并发标记、最终标记、并发清理、并发压缩等阶段,大部分阶段与用户线程并行。
- 开源与兼容性:Shenandoah由OpenJDK社区开发,对JVM代码侵入性小,兼容更多JVM特性。
Shenandoah同样追求低停顿、大内存支持,与ZGC形成"双雄并立"的局面。
第七章:垃圾回收器的选择与调优实践
7.1 回收器选择的黄金法则
应用类型 | 推荐收集器 | JVM参数示例 |
---|---|---|
客户端应用 | Serial | -XX:+UseSerialGC |
吞吐量优先 | Parallel Scavenge + Parallel Old | -XX:+UseParallelGC |
延迟敏感(小堆) | CMS | -XX:+UseConcMarkSweepGC |
延迟敏感(大堆) | G1 | -XX:+UseG1GC |
超大内存+低延迟 | ZGC/Shenandoah | -XX:+UseZGC / -XX:+UseShenandoahGC |
7.2 调优的核心思路
- 明确目标:是追求吞吐量、低延迟,还是两者平衡?
- 控制堆大小 :通过
-Xmx
(最大堆)、-Xms
(初始堆)设置合理的堆内存,避免堆过大导致GC时间长,或堆过小导致GC频繁。 - 年轻代比例 :对于G1,可通过
-XX:YoungGenerationSizeIncrement
调整年轻代增长比例;对于Parallel/CMS,可通过-XX:NewRatio
设置老年代与年轻代的比例。 - GC日志分析 :添加
-Xlog:gc*
(JDK 9+)或-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
(JDK 8),分析GC频率、停顿时间、回收效率等指标。 - 避免Full GC:Full GC通常伴随长时间STW,应通过监控(如JVisualVM、Grafana+Prometheus)及时发现并解决(如调整回收器参数、优化对象创建频率)。
结语:垃圾回收的未来演进
从Serial的单线程朴素回收,到Parallel的多线程吞吐量优化,再到CMS的并发低停顿尝试,直至G1的区域化可预测停顿,以及ZGC、Shenandoah的亚毫秒级停顿------JVM垃圾回收器的发展,始终围绕"更高效地管理内存,更友好地支持应用"这一核心目标。
随着云计算、大数据、微服务等技术的发展,应用对内存的需求越来越大,对延迟的容忍度越来越低。未来的垃圾回收器,必将在"超大内存支持""超低停顿""智能化调优"等方向持续突破,让Java在高性能领域的竞争力愈发强劲。
而作为Java开发者,深入理解垃圾回收的原理与实践,不仅能帮助我们解决线上性能问题,更能让我们在架构设计、系统优化时,做出更符合JVM运行特性的决策------这,正是探索JVM垃圾回收艺术的价值所在。