JVM垃圾回收器深度解析:从Serial到G1,探索垃圾回收的艺术

引言:为什么垃圾回收如此重要?

在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,流程如下:

  1. STW暂停用户线程:确保回收过程中对象引用不发生变化。
  2. 根节点枚举:从GC Roots出发,标记所有可达对象。
  3. 复制算法(Copying)回收Eden和Survivor
    • 将Eden和From Survivor存活的对象 ,复制到To Survivor区;
    • To Survivor空间不足,或对象年龄达到阈值(默认15,可通过-XX:MaxTenuringThreshold调整),则将对象晋升到老年代
  4. 清空Eden和From Survivor:回收这些区域的内存,供新对象分配。

2.3 老年代Serial Old收集器的回收流程

Serial Old是Serial收集器的老年代版本,采用标记-整理算法(Mark-Compact),流程为:

  1. STW暂停用户线程
  2. 标记阶段:从GC Roots出发,标记所有存活对象。
  3. 整理阶段 :将所有存活对象向一端移动,使内存形成连续的空闲区域。
  4. 清除阶段:回收移动后剩下的"空闲尾部"内存。

2.4 Serial收集器的优缺点

  • 优点:实现简单,单线程下无竞争,CPU开销小,在小堆上停顿时间短(几十毫秒内)。
  • 缺点 :STW停顿时间随堆大小增加而线性增长,大堆场景下无法接受(如堆内存为几GB时,停顿可能达数百毫秒)。

第三章:并行垃圾收集器(Parallel)

3.1 并行收集器的设计目标:吞吐量优先

Parallel收集器是Serial的多线程版本 ,核心追求是高吞吐量 (公式:用户代码执行时间 / (用户代码执行时间 + GC时间))。适合服务器端批量处理场景(如后台计算、大数据分析)。

Parallel家族包括:

  • Parallel Scavenge:年轻代收集器,多线程执行;
  • Parallel Old:老年代收集器,JDK 6后推出,支持多线程"标记-整理"。

3.2 年轻代Parallel Scavenge的回收流程

与Serial年轻代流程类似,但多线程并行回收

  1. STW暂停用户线程
  2. 多线程根节点枚举与标记:多个GC线程同时遍历引用图,标记存活对象。
  3. 多线程复制回收 :多个线程同时将Eden和From Survivor的存活对象复制到To Survivor或晋升到老年代。
  4. 清空回收区域

JVM提供参数-XX:ParallelGCThreads控制GC线程数量(默认与CPU核心数相关)。

3.3 老年代Parallel Old的回收流程

Parallel Old采用多线程标记-整理算法,流程为:

  1. STW暂停用户线程
  2. 多线程标记:多个线程同时标记存活对象。
  3. 多线程整理:多个线程协同将存活对象向一端移动,压缩内存。
  4. 清除空闲内存

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停顿时间短(初始标记和重新标记的停顿通常在几十毫秒级别)。
  • 适合延迟敏感场景,能有效提升应用响应性。
缺点与挑战:
  1. CPU资源敏感:并发阶段需要占用CPU资源,若CPU核心数少,会与用户线程争夺资源,导致应用吞吐量下降。
  2. 内存碎片问题 :采用"标记-清除"算法,回收后内存会产生碎片。当老年代碎片过多时,大对象无法分配,会提前触发Full GC(通常用Serial Old执行,停顿时间长)。
  3. 浮动垃圾问题 :并发清除阶段产生的新垃圾无法回收,只能等到下次GC,因此需要预留足够内存供用户线程运行,否则会触发"Concurrent Mode Failure"(并发回收时内存不足,转而执行Serial Old的Full GC)。
  4. 老年代回收阈值难调 :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,流程如下:

  1. STW暂停用户线程
  2. 扫描根(Root Scanning):从GC Roots出发,标记年轻代内的可达对象。
  3. 更新Remembered Set(RSet) :G1通过RSet 解决跨Region引用问题------每个Region维护一个RSet,记录"其他Region中指向本Region的引用"。回收时,只需扫描RSet即可找到外部引用,无需全堆扫描。
  4. 复制存活对象:将Eden和Survivor Region中存活的对象,复制到新的Survivor Region或老年代Region(若年龄达标)。
  5. 回收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,流程如下:

  1. STW暂停用户线程
  2. 选择回收的Region:根据停顿预测模型,选择一批"垃圾多、回收收益高"的Region(包括年轻代和老年代)。
  3. 复制存活对象:多线程将选中Region中的存活对象,复制到新的空闲Region(可能是年轻代或老年代)。
  4. 回收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 调优的核心思路

  1. 明确目标:是追求吞吐量、低延迟,还是两者平衡?
  2. 控制堆大小 :通过-Xmx(最大堆)、-Xms(初始堆)设置合理的堆内存,避免堆过大导致GC时间长,或堆过小导致GC频繁。
  3. 年轻代比例 :对于G1,可通过-XX:YoungGenerationSizeIncrement调整年轻代增长比例;对于Parallel/CMS,可通过-XX:NewRatio设置老年代与年轻代的比例。
  4. GC日志分析 :添加-Xlog:gc*(JDK 9+)或-XX:+PrintGCDetails -XX:+PrintGCTimeStamps(JDK 8),分析GC频率、停顿时间、回收效率等指标。
  5. 避免Full GC:Full GC通常伴随长时间STW,应通过监控(如JVisualVM、Grafana+Prometheus)及时发现并解决(如调整回收器参数、优化对象创建频率)。

结语:垃圾回收的未来演进

从Serial的单线程朴素回收,到Parallel的多线程吞吐量优化,再到CMS的并发低停顿尝试,直至G1的区域化可预测停顿,以及ZGC、Shenandoah的亚毫秒级停顿------JVM垃圾回收器的发展,始终围绕"更高效地管理内存,更友好地支持应用"这一核心目标。

随着云计算、大数据、微服务等技术的发展,应用对内存的需求越来越大,对延迟的容忍度越来越低。未来的垃圾回收器,必将在"超大内存支持""超低停顿""智能化调优"等方向持续突破,让Java在高性能领域的竞争力愈发强劲。

而作为Java开发者,深入理解垃圾回收的原理与实践,不仅能帮助我们解决线上性能问题,更能让我们在架构设计、系统优化时,做出更符合JVM运行特性的决策------这,正是探索JVM垃圾回收艺术的价值所在。

相关推荐
大虾别跑4 小时前
vc无法启动
java·开发语言
郭老二4 小时前
【JAVA】从入门到放弃-01-HelloWorld
java·开发语言
卷Java4 小时前
CSS模板语法修复总结
java·前端·css·数据库·微信小程序·uni-app·springboot
龙茶清欢5 小时前
7、revision 是 Maven 3.5+ 引入的现代版本管理机制
java·elasticsearch·maven
柯南二号5 小时前
【Java后端】《Spring Boot Starter 原理详解》博客
java·开发语言·spring boot
歪歪1005 小时前
如何在SQLite中实现事务处理?
java·开发语言·jvm·数据库·sql·sqlite
毕设源码-郭学长5 小时前
【开题答辩全过程】以 J2EE在电信行业的应用研究为例,包含答辩的问题和答案
java·java-ee
Aevget5 小时前
「Java EE开发指南」如何用MyEclipse开发Java EE企业应用程序?(二)
java·ide·java-ee·开发·myeclipse
不爱编程的小九九6 小时前
小九源码-springboot048-基于spring boot心理健康服务系统
java·spring boot·后端