很多人学 GC 的痛点是:
- 名词一堆:标记清除、复制、标记整理、分代
- 但一旦你真遇到"内存回不去",你又不知道该从哪里解释
这篇把 GC 的主线拆成两条:
- 先判定谁活谁死(可达性分析)
- 再决定怎么回收(算法/策略)
1. 为什么 JVM 不用引用计数:循环引用是天坑
引用计数思路:
- 每个对象维护引用次数
- 变成 0 就回收
问题:
- 两个对象互相引用,引用次数永远不为 0,但它们可能已经"不可达"
所以 HotSpot 主流使用:
- 可达性分析(Reachability Analysis)
2. 可达性分析:从 GC Roots 出发,能走到就是活
核心规则:
- 从一组根对象(GC Roots)出发遍历对象图
- 能到达的对象都是存活
- 到达不了的对象就是垃圾
2.1 常见 GC Roots 你至少要认识
- 线程栈中的引用(局部变量表)
- 静态变量引用(被类持有的引用)
- JNI 引用(本地方法栈)
- 运行中的线程对象、Class 对象等(不同实现有差异)
这也是为什么:
- 静态集合很容易造成"你以为没用了但其实一直活着"
3. 回收算法:标记清除 / 复制 / 标记整理
3.1 标记清除(Mark-Sweep)
- 标记出要回收的对象
- 直接清除
问题:
- 产生碎片
3.2 复制算法(Copying)
- 把存活对象复制到另一块连续空间
- 直接清理整块旧空间
特点:
- 回收快、无碎片
- 需要额外空间
常见用法:
- 新生代(对象存活率低,复制成本低)
3.3 标记整理(Mark-Compact)
- 标记存活对象
- 把存活对象往一侧挪,整理成连续空间
特点:
- 减少碎片
- 整理有成本
常见用法:
- 老年代(存活率高,复制成本高)
4. 分代收集:不是算法,是"策略组合"
分代思想来自经验:
- 大多数对象活不久
所以:
- 新生代:复制/快速回收
- 老年代:标记整理(或标记清除 + 处理碎片)
5. 四种引用:为什么你"以为能回收"但没回收
5.1 强引用
- 默认写法就是强引用
- 只要强引用还在,通常不会回收
5.2 软引用(SoftReference)
- 内存紧张时才回收
- 常用于缓存,但别把它当万能缓存方案
5.3 弱引用(WeakReference)
- 下一次 GC 就可能被回收
- 常见场景:弱引用缓存、ThreadLocal 的 key
5.4 虚引用(PhantomReference)
- 不影响对象生命周期
- 常用于"对象被回收前做通知/资源清理"这类高级用法
6. 把这些知识落到排障:你该如何解释"内存回不去"
你遇到"Full GC 后内存回不去",你可以按这个顺序解释:
- 先确认:是不是有 GC Roots 在引用(静态集合/线程栈/ThreadLocal)
- 再确认:存活对象是否真的很多(业务缓存/大对象)
- 最后用 heapdump + MAT 找到引用链
7. 总结
- HotSpot 主要用可达性分析,从 GC Roots 出发判定存活
- 回收算法:标记清除(碎片)/复制(快)/标记整理(少碎片)
- 分代是策略组合:新生代与老年代采用不同回收方式
- 四种引用决定"对象在不同内存压力下是否会被回收"