Java 垃圾回收(GC)算法详解

Java 垃圾回收(GC)算法详解

目标:把 “Java 是怎么判断对象能不能回收”、以及 “具体用什么算法回收” 讲清楚,并顺手把它们和 HotSpot 的分代收集、常见收集器联系起来。


1. GC 要解决的核心问题

Java 的自动内存管理主要解决两件事:

  1. 判断哪些对象“死了”(不可达)
  2. 用什么方式回收它们占用的内存(算法与实现)

真正的回收动作,本质上就是围绕这两件事在做工程取舍:吞吐量、延迟(STW)、内存碎片、CPU 开销、并发复杂度等。


2. 对象“是否存活”的判定:可达性分析(Reachability Analysis)

2.1 引用计数法(Reference Counting)——理论简单,Java 主流不使用

思路:对象有一个计数器,引用 +1,取消引用 -1,计数为 0 就回收。

致命问题:无法解决循环引用。

A -> B
B -> A

A、B 互相引用,计数都不为 0,但其实外界已经不可达了,导致内存泄漏。

HotSpot 主流采用 可达性分析 而不是引用计数。


2.2 可达性分析:从 GC Roots 出发“找活人”

思路:把一批“肯定活着的对象”当作根(GC Roots),从根出发沿引用链遍历:

  • 能走到的对象:存活
  • 走不到的对象:可回收
GC Roots 常见来源(非常重要)
  • 虚拟机栈(栈帧)中的引用(局部变量)
  • 方法区(静态变量)中的引用
  • 方法区中常量引用(字符串常量池等)
  • JNI 引用(Native 方法里持有的对象)
  • 活跃线程对象、同步锁持有的对象(如 synchronized 关联的 Monitor)

“为什么局部变量没置 null 也会被回收?”
因为 JIT 可能做 逃逸分析/标量替换/栈上分配 或者优化掉某些引用的作用域;能否回收取决于 可达性,不是你“看起来还在变量里”。


3. GC 算法的“基本动作”

不管你用什么收集器,核心操作通常就是这几个步骤的组合:

  • 标记(Mark):找出要回收/要保留的对象
  • 清除(Sweep):把不可达对象的内存释放
  • 整理(Compact):把存活对象挪一挪,消除碎片
  • 复制(Copy):把存活对象复制到另一块区域(天然紧凑)

不同算法的差异,就是:
怎么标记?怎么释放?是否移动对象?是否产生碎片?是否需要额外空间?是否能并发?


4. 四大经典 GC 算法

4.1 标记-清除(Mark-Sweep)

流程

  1. 从 GC Roots 标记存活对象
  2. 扫描堆,把未标记对象的内存 清除

优点

  • 实现简单
  • 不需要移动对象(对象地址稳定)

缺点

  • 内存碎片多:大量不连续空洞,影响分配效率
  • 清除阶段可能扫描成本高
回收前: [A][B][C][D][E]
回收后: [A][ ][C][ ][E]   -> 产生碎片

适用场景

  • 对象移动成本高的场景(但现代 JVM 更多用整理或区域化来解决碎片)

4.2 标记-整理(Mark-Compact)

流程

  1. 标记存活对象
  2. 把存活对象往一侧 紧凑搬迁
  3. 直接清理掉边界外的空间

优点

  • 无碎片(内存连续)
  • 分配效率高(bump-the-pointer 线性分配)

缺点

  • 移动对象成本高:需要更新引用(指针修正)
  • STW 时间可能更长(实现也更复杂)
回收前: [A][ ][C][ ][E]
整理后: [A][C][E][ ][ ]

适用场景

  • 老年代回收常用(对象存活率高,复制不划算,碎片又不能忍)

4.3 复制算法(Copying / Semi-space)

流程

  • 把内存分成两块:From、To
  • 只用 From 分配对象
  • 回收时把存活对象复制到 To,并让它们紧凑排列
  • 交换 From/To

优点

  • 回收速度快(只复制存活对象)
  • 天然无碎片
  • 分配非常快(线性指针)

缺点

  • 空间浪费:需要一块等大备用区(理论上 50%)
  • 对象存活率高时复制成本高

适用场景

  • 新生代(大部分对象“朝生夕死”,存活率低,复制非常划算)

4.4 分代收集(Generational Collection)

这不是单独的一种“微观算法”,而是一种宏观策略
基于经验规律:

绝大多数对象生命周期很短
熬过多次 GC 的对象更可能长期存活

因此把堆分成:

  • 新生代(Young):对象创建集中、死亡率高 → 用复制算法
  • 老年代(Old):对象存活率高 → 用标记-清除/标记-整理/区域化算法
新生代的典型结构(HotSpot 常见)
  • Eden
  • Survivor0(S0)
  • Survivor1(S1)

对象一般在 Eden 分配;Minor GC 时,把 Eden + 一个 Survivor 里的存活对象复制到另一个 Survivor;达到阈值就晋升到老年代。


5. 现代 GC 里的关键技术点(算法落地必备)

5.1 STW(Stop-The-World)

为了保证对象图一致性,GC 在某些阶段需要暂停应用线程(Mutator)。
目标就是:尽量缩短 STW,甚至让多数工作并发做完


5.2 写屏障(Write Barrier)与记忆集(Remembered Set)

分代收集有个麻烦:老年代对象可能引用新生代对象。

Minor GC 只回收新生代时,如果每次都全堆扫描找这种引用,成本爆炸。

解决:写屏障 + Card Table / Remembered Set

  • 对象引用发生写入时(如 oldObj.field = youngObj),通过写屏障把对应“卡页”标记为脏
  • Minor GC 时只扫描这些脏卡对应的老年代区域即可

5.3 三色标记(Tri-color Marking)与并发标记问题

并发标记时,应用线程也在改引用,会出现:

  • 漏标(本该活着但没标到) → 这是致命错误
  • 多标(本该死但被标了) → 只是浮动垃圾(下一轮再收)

现代并发 GC(如 CMS/G1/ZGC/Shenandoah)都会用:

  • SATB(Snapshot-At-The-Beginning)
  • 增量更新(Incremental Update)
    配合写屏障来解决一致性问题。

6. 把“算法”映射到 HotSpot 常见收集器(快速对号入座)

收集器(Collector)是“工程实现”,它们内部会组合使用上面的算法。

6.1 Serial / ParNew(新生代)

  • 复制算法
  • STW
  • Serial 单线程;ParNew 多线程

6.2 Parallel Scavenge(新生代)

  • 复制算法
  • 目标偏吞吐量(Throughput)

6.3 Serial Old / Parallel Old(老年代)

  • 标记-整理 为主
  • STW
  • 主要用于吞吐量场景

6.4 CMS(老年代,已逐步被替代)

  • 标记-清除 + 并发标记
  • 优点:低停顿
  • 缺点:碎片问题、并发失败(Concurrent Mode Failure)、对 CPU 压力大

6.5 G1(面向服务端,默认/主流之一)

  • 堆划分为多个 Region
  • 整体是 “分代 + 区域化 + 标记整理(局部)”
  • 通过预测模型控制停顿时间(Pause Time Goal)
  • 通过 Mixed GC 同时回收部分老年代 Region

6.6 ZGC / Shenandoah(超低停顿)

  • 重点是“并发移动对象 + 并发标记”
  • 依赖读/写屏障与复杂的指针染色等技术
  • 目标:极低 STW(通常毫秒级甚至更低),代价是实现复杂、对平台/版本依赖更强

面试时可以这么收口:
经典算法讲原理;收集器讲工程权衡;新生代倾向复制,老年代倾向整理/区域化;低停顿靠并发 + 屏障。


7. 常见问题与面试“易错点”

7.1 为什么新生代适合复制,老年代不适合?

  • 新生代:存活率低 → 复制少量对象,成本低,速度快
  • 老年代:存活率高 → 复制对象多,成本高,还需要等大备用空间,不划算

7.2 标记-清除为什么会影响性能?

  • 碎片导致分配慢,可能触发更多 GC
  • 大对象分配可能因为找不到连续空间而失败(需要整理或 Full GC)

7.3 什么是浮动垃圾?

并发标记期间,新产生的垃圾可能没被本轮标记识别出来(例如 SATB 语义下),它们会留到下一轮回收。
这不是 bug,是并发 GC 的设计权衡。


8. 一句话总结

  • 存活判定:可达性分析(GC Roots)
  • 基础算法:标记-清除、标记-整理、复制
  • 宏观策略:分代收集(年轻代复制、老年代整理/区域化)
  • 现代优化:并发标记 + 写屏障 + 记忆集 + 三色标记一致性

9. 推荐延伸阅读(可选)

  • 《深入理解 Java 虚拟机》(周志明)—— GC 部分是面试高频
  • OpenJDK / HotSpot 源码 & JEP:了解 G1/ZGC 的设计动机与边界
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值