延安市网站建设_网站建设公司_原型设计_seo优化
2025/12/18 11:24:51 网站建设 项目流程

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等影响

对象通常的“命运”:

  1. 大部分对象在Eden 分配
  2. 第一次/多次 Young GC 后存活,进入Survivor,对象“年龄”增加
  3. 达到阈值或 Survivor 放不下 →晋升(Promote)到 Old
  4. Old 压力大 → Mixed/Old GC 或最终 Full GC
  5. 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)

  1. 线程优先在 TLAB 分配
    • TLAB(Thread Local Allocation Buffer)是线程私有的小块 Eden 切片
    • 好处:分配时几乎无需加锁,快
  2. TLAB 不够就去 Eden 公共区域分配
  3. Eden 也不够 → 触发 Young GC(通常是 STW)

Insight:你看到的“GC”其实往往是“分配失败的后果”,所以排查 GC 频繁,要从“分配速度”和“存活率”入手。


3.2 进入安全点:Stop-The-World(STW)

Young GC 多数情况下需要 STW(即使某些回收器有并发阶段,关键阶段仍要停)。

大致步骤:

  1. JVM 发起 GC 请求
  2. 各线程运行到Safepoint(安全点)停下(或被抢占到安全点)
  3. 保存线程状态,进入 GC 线程执行回收

3.3 根扫描(Root Scanning)

GC 的第一件大事:找到“仍然活着的对象”的入口。

GC Roots 常见来源

  • 各线程栈上的引用(局部变量、参数)
  • 静态变量引用(static
  • JNI 引用
  • 类加载器、系统类等内部结构
  • 同步锁持有的对象(monitor)
  • 处理中的引用队列(finalizer/Reference 等)

这一步的目标:得到“活对象集合”的起点,然后向下遍历对象图。


3.4 标记存活对象(Mark)

  1. 从 Roots 出发遍历对象引用关系
  2. 被访问到的对象标记为“存活”
  3. 未被标记的对象视为“垃圾”

注意:在分代回收中,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)与对象年龄

常见的年轻代回收是“复制算法”:

  1. Eden 中存活对象复制到 Survivor(目标 S 区)
  2. Survivor(from)中的存活对象复制到 Survivor(to)
  3. 每复制一次对象,年龄 age++
  4. 如果 Survivor 放不下,或 age 达到阈值(MaxTenuringThreshold等),则对象晋升到老年代

这里会发生你最关心的点:晋升压力
如果老年代空间不足以容纳晋升对象,就可能触发更重的 GC(甚至 Full GC)。


3.7 引用处理与 Finalization(常被忽略但很关键)

GC 过程中需要专门处理:

  • SoftReference/WeakReference/PhantomReference
  • finalize()(历史包袱,强烈不建议依赖)

这些会涉及 ReferenceQueue、Finalizer 队列等,可能引入额外开销和不可控延迟。


3.8 清理与重置:回收 Eden / From Survivor

  1. Eden、From Survivor 的空间整体“清空”(逻辑上回收)
  2. To Survivor 成为新的 From Survivor(交换角色)
  3. 更新分代边界信息、统计信息(如年龄分布)

3.9 恢复执行:退出 STW

  1. GC 线程结束本次回收
  2. 解除 safepoint,恢复业务线程
  3. 继续对象分配与执行

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 多是:

  1. STW
  2. Roots 扫描
  3. Mark(标记)
  4. Sweep(清除):回收未标记对象
  5. Compact(压缩):整理内存,消除碎片,更新引用
  6. 恢复执行

Insight:Full GC 痛点在于“老年代对象多 + 需要整理引用/压缩”,不是简单清理那么轻松。


5. 把一次“完整 GC”串成一条时间线(从业务视角)

下面是你在生产上经常遇到的一条完整链路(概念版):

  1. 业务线程高速创建对象 → Eden 增长
  2. Eden 满 → Young GC(STW)
  3. 存活对象进入 Survivor,部分晋升到 Old
  4. 老年代逐渐膨胀
  5. 到达阈值 → 启动并发标记(G1/CMS 等)
  6. 多次 Mixed GC / Old GC 清理老年代垃圾
  7. 如果并发回收跟不上 / 晋升过快 / 空间碎片严重
    → 退化为 Full GC(STW,最重)
  8. Full GC 后如果仍无法分配
    OutOfMemoryError(堆/元空间/直接内存等)

6. 你在 GC 日志里会看到什么(关键词对照)

常见关键词(不同回收器输出不完全一致):

  • Pause Young (Normal):正常年轻代停顿
  • Pause Young (Allocation Failure):分配失败触发 Young GC
  • Pause Young (Mixed):混合回收(G1)
  • Concurrent Mark Cycle:并发标记周期开始(G1)
  • Remark/Cleanup:再标记/清理
  • Full GC:全量回收(STW,通常最重)
  • Promotion failed/to-space exhausted:晋升/转移失败信号(危险)

7. “完整 GC 流程”最常见的性能瓶颈点(排查方向)

  1. 对象分配速率过高
    • 大量短命对象 → Young GC 频繁但不一定坏(看停顿)
  2. 对象存活率过高
    • Survivor 装不下 → 晋升多 → Old 快满
  3. 老年代回收跟不上
    • Mixed 次数增多/停顿变长
  4. 并发回收退化为 Full GC
    • 直接导致延迟飙升
  5. 元空间/类卸载问题
    • 动态类过多导致 Full GC 或 OOM Metaspace
  6. 直接内存 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 节日志关键词,结合“触发点 → 流程阶段 → 瓶颈点”定位问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询