JVM 的 GC(Garbage Collection)机制是 Java 程序性能的关键支柱。本文将从堆内存布局、回收原理、GC 算法、流程细节、并发收集器机制等维度,系统讲清楚 GC 的底层运作原理和优化思路。
一、JVM 堆内存结构
Java 堆是 GC 管理的主要区域,按生命周期划分为以下几个部分:
1.1 年轻代(Young Generation)
Eden 区:新对象最初分配在此
Survivor 区:分为 S0 / S1 两个区,用于对象在晋升前的多次存活
1.2 老年代(Old Generation)
存放从年轻代晋升上来的长寿对象
内存大,GC 频率低,但成本高
1.3 元空间(MetaSpace)
存放类元数据(Class 对象、方法表等)
从 JDK 8 起替代了 PermGen 永久代
二、对象如何判断是否可以被回收?
2.1 可达性分析(Reachability Analysis)
GC 会从一组 “GC Roots” 对象开始向下追踪引用链,凡是无法从 GC Roots 可达的对象,被认为是“垃圾”。
GC Roots 主要包括:
当前线程的局部变量(栈帧)
类的静态字段引用
JNI 本地方法引用
2.2 引用计数法(已废弃)
引用计数法 (Reference Counting) 的基本原理是:
给每个对象维护一个引用计数器。
每当有一个引用指向这个对象时,计数器 +1。
当一个引用不再指向这个对象时,计数器 -1。
当计数器为 0 时,说明没有任何引用指向该对象,它就是不可达的,可以被回收。
虽然概念简单(对象被引用次数为 0 即可回收),但无法解决循环引用问题,因此主流 JVM 都使用可达性分析。
最主要的缺点是:无法处理循环引用问题。
示例:
class Node {
Node next;
}
Node node1 = new Node();
Node node2 = new Node();
node1.next = node2;
node2.next = node1;
node1 和 node2 互相引用。
即使你把它们的外部引用都置为 null,它们的引用计数都不为 0。
但这两个对象实际上是不可达的,应该被回收。
⚠️ 引用计数法无法检测这种情况,因此内存泄漏会发生。
三、GC 类型与时机
3.1 Minor GC(小回收)
回收年轻代
使用 复制算法
快速、频繁、STW
3.2 Major GC / Old GC / Mixed GC(大回收)
回收老年代
可能是并发或 STW,取决于收集器
3.3 Full GC(整堆回收)
回收整个堆(年轻代 + 老年代 + 元空间)
STW,性能开销大
触发原因:
老年代内存不足
调用 System.gc()
CMS GC 失败
四、常见 GC 算法
4.1 复制算法(Copying)
年轻代使用(Eden ➜ Survivor)
每次回收只处理 Eden + 一个 Survivor,存活对象复制到另一个 Survivor 或晋升老年代
简单高效,适合“朝生夕灭”的对象
4.2 标记-清除(Mark-Sweep)- 只有这种算法才能并行(Initial Mark and Initial Mark)
老年代基础算法
缺点:产生碎片
4.3 标记-整理(Mark-Compact)
在清除后整理内存,消除碎片
用于老年代(如 Serial Old、G1 Old)
五、Stop-The-World (STW)
GC 期间 JVM 会暂停所有应用线程,这称为 Stop-The-World
即使是并发 GC(如 CMS、G1)也不能完全避免 STW,尤其在:
初始标记(Initial Mark)
重新标记(Remark)
Full GC
ZGC垃圾回收周期如下图所示:
ZGC只有三个STW阶段:初始标记,再标记,初始转移.
六、GC 的完整流程细节(以 G1 为例)
6.1 触发 GC(如 Eden 区满)
Minor GC 开始
STW,复制 Eden 区存活对象 ➜ Survivor 区
Survivor 满或年龄够 ➜ 晋升老年代
释放 Eden 区对象
6.2 并发标记流程(预防 Full GC)
当老年代占用达到一定阈值时,G1 启动 Concurrent Marking 过程:
阶段描述是否 STWInitial Mark标记 GC Roots 引用对象✅ 是Concurrent Mark遍历对象图,标记可达对象❌ 否Remark捕获并发阶段变更的引用✅ 是Cleanup清理不可达的 Region❌ 否
七、三色标记法(Tri-color Marking)
为支持并发标记,GC 使用三色标记算法确保安全性:
白色:未被访问(假定为垃圾)
灰色:被访问但未扫描其引用
黑色:自身与其引用都已处理
写屏障(SATB、增量更新)
用于记录并发阶段引用变更,防止“误删活对象”
八、对象晋升与回收细节
对象在 Survivor 区每存活一次,年龄 +1
达到 MaxTenuringThreshold 或 Survivor 区满时晋升老年代
G1 Mixed GC 会“部分回收”老年代,非 Full GC
九、避免 Full GC 的实践建议
设置合适的堆大小:避免频繁 Minor 或 Full GC
使用 -XX:+PrintGCDetails 观察 GC 频率和时间
针对 G1,关注老年代使用率和 Mixed GC 是否充分触发
防止大对象直接进入老年代(如 -XX:+UseLargePages、避免一次性创建大数组)
🔚 总结
JVM GC 机制既有实时性需求,也有吞吐压力。掌握对象生命周期、GC 类型和回收策略,是 Java 性能调优的核心技能。随着 G1、ZGC、Shenandoah 等收集器的引入,并发、可预测、低延迟的垃圾回收将成为主流。
🚫 使用 G1 GC 时的反面案例与问题分析
例 1:大量短命小对象 + 高分配速率,结果频繁 Minor GC
背景:
某服务使用 G1 GC,业务为高并发 API 网关
每秒创建数万个小对象(如 JSON 解析、临时集合等)
错误表现:
Eden 区很快满,导致 频繁 Minor GC
老年代增长不快,但 GC 次数持续拉高
响应时延抖动明显
原因分析:
G1 在默认配置下 Eden 占比较小,无法适应高速对象分配,导致频繁 GC(即便每次 GC 都很快)
优化建议:
-XX:G1NewSizePercent=30 # 提高年轻代初始比例
-XX:G1MaxNewSizePercent=60 # 增大年轻代最大值
-XX:MaxGCPauseMillis=100 # 适当放宽暂停时间目标
例 2:大对象直接分配到老年代,导致提前 Full GC
背景:
某报表系统一次性构造数十 MB 的 List
JVM 使用 G1,老年代频繁增长
错误表现:
年轻代未满,但大对象直接进老年代
老年代迅速填满,触发 Full GC
G1 试图执行 Mixed GC,但回收效果不佳
原因分析:
G1 对大于 region size 的对象(默认 1~32MB),会直接分配到老年代
如果这些对象生命周期短,会造成老年代“脏积压”
优化建议:
-XX:G1HeapRegionSize=8m # 适当增大 Region Size
-XX:G1ReservePercent=20 # 增加保留内存,防止老年代爆掉
-XX:+UseStringDeduplication # 优化大量重复字符串的内存使用
例 3:设置了极端的 MaxGCPauseMillis,导致 GC 频繁打断业务
背景:
运维强行要求 GC 停顿 < 20ms,于是配置:
-XX:MaxGCPauseMillis=20
错误表现:
G1 尝试做“小步快跑”,每次只回收很少区域
GC 调度频繁,导致 GC 开销过大
实际吞吐率反而降低,甚至 Full GC 也变得更频繁
原因分析:
G1 的暂停预测模型被“过度限制”
GC 频繁被触发,但每次清理量不够,长远看反而回收不及时
优化建议:
放宽 MaxGCPauseMillis 至 100ms 以内,兼顾吞吐和暂停
观察日志中的 “pause target met” 状态,动态调整
例 4:Mixed GC 没有完成,导致 Full GC 重复触发
背景:
应用负载波动大,Mixed GC 经常被中断
老年代利用率飙升,最终触发多次 Full GC
错误表现:
G1 执行并发标记后启动 Mixed GC
但在处理 Eden 区时就被打断,老年代未清理
多轮循环 GC 后,内存碎片堆积 ➜ Full GC
原因分析:
Mixed GC 本质上是 “老年代的增量回收”
如果每轮 GC 清理量太少,无法及时清理老年代,会累积到需要 Full GC
优化建议:
-XX:G1HeapWastePercent=5 # 允许适度“浪费”,让更多 Region 被选中
-XX:G1MixedGCLiveThresholdPercent=85 # 调整可回收区域的判定阈值
-XX:G1OldCSetRegionThresholdPercent=20 # 每轮最大处理老年代比例
其它
JVM 从诞生到现在,垃圾回收的算法有且只有三种。Mark-Sweep, Mark-Compact, Coping.
只有Mark-sweep 才有和可能和业务代码并行执行。
JVM 从诞生到现在,使用这三种算法,结合分区或分代模型,出现了10种垃圾回收器。
左边6个是分代的垃圾回收器,右边四个是分区的垃圾回收器(G1, ZGC, Shenandoah, Eplison)
Region 分区, Generation: 分代。
分区模型的使用场景是大内存。
E : Eden, O: old, S: Survisor, H: Humongous (存放大对象)
CMS (Concurrent Mark Sweep)
在并发标记的过程中,使用三色标记来记录每个对象有没有被扫到。
CMS 在并发标记的时候两个问题。
1. 浮动垃圾, 解决方法是下次垃圾回收再清除。
2. 漏标的问题。
漏标记的解放方式是incremental update ,也就是如果发现有新增的引用在黑色标记的对象的时候,变成灰色。
但是这种方法还是有问题。也有可能有漏标的问题。
彻底解决方法是从 gc root 开始重新在扫描一遍。 所以说CMS 不稳定。
并行标记清除算法不会标记整理。内存碎片的问题。
如果在使用CMS的时候,没法放入大对象,那么它会回退到Serial old,
G1 垃圾回收。
在并发标记的时候产生一个的SATB 堆栈快照, 在并发标记产生的漏表的问题。在最终标记里解决掉。
深入理解 JVM 的垃圾收集器:CMS、G1、ZGC | 二哥的Java进阶之路
Java Garbage Collection Logs & How to Analyze Them - Sematext
新一代垃圾回收器ZGC的探索与实践 - 美团技术团队