Java 垃圾回收(GC)算法详解
目标:把 “Java 是怎么判断对象能不能回收”、以及 “具体用什么算法回收” 讲清楚,并顺手把它们和 HotSpot 的分代收集、常见收集器联系起来。
1. GC 要解决的核心问题
Java 的自动内存管理主要解决两件事:
- 判断哪些对象“死了”(不可达)
- 用什么方式回收它们占用的内存(算法与实现)
真正的回收动作,本质上就是围绕这两件事在做工程取舍:吞吐量、延迟(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)
流程:
- 从 GC Roots 标记存活对象
- 扫描堆,把未标记对象的内存 清除
优点
- 实现简单
- 不需要移动对象(对象地址稳定)
缺点
- 内存碎片多:大量不连续空洞,影响分配效率
- 清除阶段可能扫描成本高
回收前: [A][B][C][D][E]
回收后: [A][ ][C][ ][E] -> 产生碎片
适用场景
- 对象移动成本高的场景(但现代 JVM 更多用整理或区域化来解决碎片)
4.2 标记-整理(Mark-Compact)
流程:
- 标记存活对象
- 把存活对象往一侧 紧凑搬迁
- 直接清理掉边界外的空间
优点
- 无碎片(内存连续)
- 分配效率高(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 的设计动机与边界
5万+

被折叠的 条评论
为什么被折叠?



