JVM 中一次“完整 GC 流程”详解(从分配到回收)
这里的“完整 GC 流程”不是指某个固定的“统一步骤”(不同垃圾回收器实现差异很大),而是用最常见的分代 HotSpot JVM 视角,把一次 GC 从“为什么触发”到“如何停顿/并发/回收/整理/恢复执行”串起来讲清楚。
你可以把它理解为:**对象从出生(分配)→ 青年回收(Young/Minor GC)→ 晋升到老年代 → 混合回收(Mixed)→ 退化/全量回收(Full GC)**的完整生命线。
1. JVM 内存与对象生命周期复习(用于理解 GC 流程)
典型 HotSpot 分代堆(逻辑视角):
- Young Generation(年轻代)
- Eden(伊甸园)
- Survivor(S0 / S1,两个幸存区)
- Old Generation(老年代)
- (可选)Metaspace(元空间):类元数据,不在 Java 堆里(JDK8+)
- (可选)Direct Memory(直接内存):NIO/Netty 常用,不在堆里,受
-XX:MaxDirectMemorySize等影响
对象通常的“命运”:
- 大部分对象在Eden 分配
- 第一次/多次 Young GC 后存活,进入Survivor,对象“年龄”增加
- 达到阈值或 Survivor 放不下 →晋升(Promote)到 Old
- Old 压力大 → Mixed/Old GC 或最终 Full GC
- Full GC 可能同时涉及Old + Metaspace(甚至触发类卸载)
2. GC 触发点:为什么会发生一次 GC?
2.1 Young/Minor GC 的典型触发
- Eden 空间不足(最常见)
Allocation Failure:无法为新对象分配内存(TLAB/eden)
2.2 Mixed GC / Old GC / Full GC 的典型触发
- 老年代占用达到回收器阈值(例如 G1 触发 Mixed)
- 晋升失败(Promotion Failure):Young GC 后需要晋升到 Old,但 Old 放不下
- 并发回收来不及导致退化(例如 CMS 的
concurrent mode failure,G1 的to-space exhausted) System.gc()(可被-XX:+DisableExplicitGC影响)- 元空间压力(类加载太多、动态代理、频繁生成类等)导致的 Full GC / 类卸载
3. 一次典型 Young GC 的完整流程(分代、复制/转移)
下面流程以“分代 + 复制(Copying)/转移”的思路讲(Serial/ParNew/Parallel Scavenge/G1 的年轻代回收在概念上都类似)。
3.1 前置:对象分配(TLAB → Eden)
- 线程优先在 TLAB 分配
- TLAB(Thread Local Allocation Buffer)是线程私有的小块 Eden 切片
- 好处:分配时几乎无需加锁,快
- TLAB 不够就去 Eden 公共区域分配
- Eden 也不够 → 触发 Young GC(通常是 STW)
Insight:你看到的“GC”其实往往是“分配失败的后果”,所以排查 GC 频繁,要从“分配速度”和“存活率”入手。
3.2 进入安全点:Stop-The-World(STW)
Young GC 多数情况下需要 STW(即使某些回收器有并发阶段,关键阶段仍要停)。
大致步骤:
- JVM 发起 GC 请求
- 各线程运行到Safepoint(安全点)停下(或被抢占到安全点)
- 保存线程状态,进入 GC 线程执行回收
3.3 根扫描(Root Scanning)
GC 的第一件大事:找到“仍然活着的对象”的入口。
GC Roots 常见来源:
- 各线程栈上的引用(局部变量、参数)
- 静态变量引用(
static) - JNI 引用
- 类加载器、系统类等内部结构
- 同步锁持有的对象(monitor)
- 处理中的引用队列(finalizer/Reference 等)
这一步的目标:得到“活对象集合”的起点,然后向下遍历对象图。
3.4 标记存活对象(Mark)
- 从 Roots 出发遍历对象引用关系
- 被访问到的对象标记为“存活”
- 未被标记的对象视为“垃圾”
注意:在分代回收中,Young GC 通常只回收 Young,但对象引用可能跨代:
- 老年代对象引用年轻代对象(Old → Young)
这会影响 Young GC 的 Root 集合范围
3.5 处理跨代引用:Remembered Set / Card Table
为了避免每次 Young GC 都扫描整个老年代:
- JVM 用 **Card Table(卡表)**记录“老年代哪些区域写过指向年轻代的引用”
- Young GC 时只扫描“脏卡”对应的区域 → 作为额外 Roots
这依赖写屏障(Write Barrier):
- 当你写一个引用字段(
obj.field = newObj)时,JIT 会插入记录逻辑,把对应卡标记为 dirty
3.6 复制/转移(Copy / Evacuate)与对象年龄
常见的年轻代回收是“复制算法”:
- Eden 中存活对象复制到 Survivor(目标 S 区)
- Survivor(from)中的存活对象复制到 Survivor(to)
- 每复制一次对象,年龄 age++
- 如果 Survivor 放不下,或 age 达到阈值(
MaxTenuringThreshold等),则对象晋升到老年代
这里会发生你最关心的点:晋升压力
如果老年代空间不足以容纳晋升对象,就可能触发更重的 GC(甚至 Full GC)。
3.7 引用处理与 Finalization(常被忽略但很关键)
GC 过程中需要专门处理:
SoftReference/WeakReference/PhantomReferencefinalize()(历史包袱,强烈不建议依赖)
这些会涉及 ReferenceQueue、Finalizer 队列等,可能引入额外开销和不可控延迟。
3.8 清理与重置:回收 Eden / From Survivor
- Eden、From Survivor 的空间整体“清空”(逻辑上回收)
- To Survivor 成为新的 From Survivor(交换角色)
- 更新分代边界信息、统计信息(如年龄分布)
3.9 恢复执行:退出 STW
- GC 线程结束本次回收
- 解除 safepoint,恢复业务线程
- 继续对象分配与执行
4. 如果这次 Young GC 不够:Mixed / Old / Full GC 的“完整链路”
当对象存活率高、晋升快或老年代积压,GC 会进入更重的阶段。
4.1 G1:从 Young 到 Mixed 的典型完整流程(最常见生产配置之一)
G1 的堆被划分为许多Region(不再是固定 Young/Old 大块),但逻辑上仍是分代。
一次典型“完整链路”可能是:
(1) Young GC(STW)
- 主要回收 Eden Regions
- 可能回收部分 Survivor Regions
- 对象转移(Evacuation)
(2) 并发标记周期(Concurrent Mark Cycle)
当老年代占用达到阈值,G1 启动并发标记:
- Initial Mark(初始标记):STW(很短),标记 Roots 直达对象,并触发 SATB 相关机制
- Concurrent Mark(并发标记):与业务线程并发,遍历对象图
- Remark(再标记):STW,修正并发期间遗漏(结合 SATB/写屏障)
- Cleanup(清理):统计各 Region 的存活率,决定哪些 Old Region “最值得回收”
(3) Mixed GC(STW,多次发生)
- 每次 Mixed 会回收:Young Regions + 一部分“垃圾占比高”的 Old Regions
- 目标:用可控停顿,把老年代垃圾逐步清掉,避免一次超长 Full GC
Mixed 的核心:“挑最划算的老年代 Region 回收”(Garbage First 的名字来源)
(4) 退化到 Full GC(最不想见到)
如果发生:
to-space exhausted(转移目标空间不足)- 并发标记来不及,老年代持续膨胀
- 内存碎片/元空间等问题
G1 可能触发Full GC(STW,Mark-Compact),停顿会明显变长。
4.2 Parallel/Serial:Full GC 的典型流程(Mark-Sweep-Compact)
传统 Full GC 多是:
- STW
- Roots 扫描
- Mark(标记)
- Sweep(清除):回收未标记对象
- Compact(压缩):整理内存,消除碎片,更新引用
- 恢复执行
Insight:Full GC 痛点在于“老年代对象多 + 需要整理引用/压缩”,不是简单清理那么轻松。
5. 把一次“完整 GC”串成一条时间线(从业务视角)
下面是你在生产上经常遇到的一条完整链路(概念版):
- 业务线程高速创建对象 → Eden 增长
- Eden 满 → Young GC(STW)
- 存活对象进入 Survivor,部分晋升到 Old
- 老年代逐渐膨胀
- 到达阈值 → 启动并发标记(G1/CMS 等)
- 多次 Mixed GC / Old GC 清理老年代垃圾
- 如果并发回收跟不上 / 晋升过快 / 空间碎片严重
→ 退化为 Full GC(STW,最重) - Full GC 后如果仍无法分配
→OutOfMemoryError(堆/元空间/直接内存等)
6. 你在 GC 日志里会看到什么(关键词对照)
常见关键词(不同回收器输出不完全一致):
Pause Young (Normal):正常年轻代停顿Pause Young (Allocation Failure):分配失败触发 Young GCPause Young (Mixed):混合回收(G1)Concurrent Mark Cycle:并发标记周期开始(G1)Remark/Cleanup:再标记/清理Full GC:全量回收(STW,通常最重)Promotion failed/to-space exhausted:晋升/转移失败信号(危险)
7. “完整 GC 流程”最常见的性能瓶颈点(排查方向)
- 对象分配速率过高
- 大量短命对象 → Young GC 频繁但不一定坏(看停顿)
- 对象存活率过高
- Survivor 装不下 → 晋升多 → Old 快满
- 老年代回收跟不上
- Mixed 次数增多/停顿变长
- 并发回收退化为 Full GC
- 直接导致延迟飙升
- 元空间/类卸载问题
- 动态类过多导致 Full GC 或 OOM Metaspace
- 直接内存 OOM
- 堆看起来没满,但系统内存吃光
8. 结尾给你一张“脑内流程图”(ASCII)
对象分配(TLAB/Eden) | v Eden 不够? ----否----> 继续跑 | 是 v Safepoint -> STW | v Root 扫描 + 处理跨代引用(RSet/Card) | v 标记存活对象(Mark) | v 复制/转移到 Survivor / 晋升到 Old | v 引用处理(Soft/Weak/Phantom) + Finalize队列 | v 清空 Eden/From + 交换 Survivor | v 恢复执行(退出STW) | v Old 增长到阈值? -> 并发标记 -> Mixed GC 多次 | v 极端情况: 转移失败/并发来不及/碎片严重 -> Full GC(STW)9. 建议你怎么用这份文档
- 面试:按第 3 节(Young GC)+ 第 4 节(G1 Mixed/Full)讲,基本够打。
- 线上排查:对照第 6 节日志关键词,结合“触发点 → 流程阶段 → 瓶颈点”定位问题。