GC算法
1. 这是什么
GC 算法用于识别和回收不再使用的对象内存。
它是 JVM 自动内存管理能力的核心基础。
一句话理解:
- GC 不是“随机删对象”
- 它首先要判断对象还能不能从根出发被找到,然后再决定怎么回收
2. 为什么重要
理解 GC 算法,才能看懂:
- 不同收集器为什么表现不同
- 为什么会有停顿和吞吐的权衡
- 为什么会有内存碎片
- 为什么新生代和老年代常常采用不同策略
这也是分析这些问题的前提:
- Full GC 频繁
- 停顿时间过长
- 内存碎片严重
3. 先建立直觉:GC 不是“对象置 null 就立刻删除”
很多初学者对 GC 的第一印象是:
- 对象引用设成
null - JVM 就会马上删除它
这不准确。
更正确的理解是:
- 对象是否可回收,取决于它是否还能从 GC Roots 出发被找到
- 即使已经不可达,也不代表会立刻马上回收
所以 GC 里最关键的第一步不是“怎么删”,而是:
- 怎么判断对象还能不能被访问到
4. 核心内容
4.1 可达性分析
现代 JVM 主要通过可达性分析判断对象是否存活。
你可以先把它理解成:
- 从一组根对象出发
- 沿引用关系往下找
- 能找到的对象就是可达对象
- 找不到的对象才有资格被回收
常见 GC Roots 可以先这样理解:
- 栈中的引用
- 静态字段引用
- 本地方法相关引用
- 某些运行时系统对象
所以重点不是“有没有名字”,而是:
- 有没有从根一路引用到它
4.2 标记-清除算法
思路很直接:
- 先标记出可回收对象
- 再把这些对象占用的空间清掉
优点:
- 实现思路相对直接
缺点:
- 可能产生内存碎片
也就是说,空间虽然总量够,但可能不连续。
4.3 标记-整理算法
思路是:
- 标记出存活对象
- 把存活对象往一端移动
- 清理掉边界外的空间
优点:
- 能减少内存碎片
缺点:
- 对象移动有成本
4.4 复制算法
思路是:
- 只使用一部分空间
- 回收时把存活对象复制到另一块区域
- 直接清空原区域
优点:
- 回收简单
- 没有碎片
- 对“存活对象少”的场景很高效
缺点:
- 需要额外可用空间
这也是为什么它特别适合:
- 新生代
因为新生代里大多数对象本来就活不久,真正要复制的存活对象通常不多。
4.5 分代回收思想
分代思想不是单一算法,而是一种非常重要的组合策略:
- 大多数对象朝生夕死
- 少量对象长期存活
因此可以把堆按对象年龄特征分区:
- 新生代
- 老年代
然后:
- 新生代更适合偏复制类思路
- 老年代更重视碎片控制和长期存活对象处理
4.6 为什么不同算法要做权衡
GC 算法之间不存在“绝对最优”,只有:
- 更适合某类对象分布
- 更适合某类延迟目标
典型权衡维度包括:
- 停顿时间
- 吞吐量
- 空间利用率
- 碎片情况
- 对象移动成本
5. 学习重点
这一章最重要的是掌握这些判断:
- JVM 如何判断对象是否可以回收
- 标记-清除、标记-整理、复制算法的核心差别
- 内存碎片是怎么来的
- 为什么复制算法适合新生代
- 为什么 GC 常常要结合分代思想
6. 常见问题
6.1 把引用为 null 等同于一定会被立即回收
这只是让对象可能变得不可达,不代表 JVM 会立刻回收它。
6.2 不了解算法差异,只记名字
只知道“有这些算法”,但不知道它们分别在解决什么问题,是最常见的学习卡点。
6.3 不理解碎片和压缩的关系
碎片本质上是:
- 空闲空间总量够
- 但不连续
这也是标记-整理价值所在。
7. 动手验证
这一节可以直接复制运行,边看边验证。
7.1 准备一个可运行示例
新建文件 GcAlgorithmDemo.java,内容如下:
java
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class GcAlgorithmDemo {
private static final List<byte[]> LONG_LIVED = new ArrayList<>();
public static void main(String[] args) throws Exception {
Object target = new Object();
WeakReference<Object> weak = new WeakReference<>(target);
System.out.println("weakBeforeClear=" + (weak.get() != null));
target = null;
System.gc();
Thread.sleep(200);
System.out.println("weakAfterGc=" + (weak.get() == null));
for (int i = 0; i < 3; i++) {
allocateShortLived();
}
System.out.println("shortLivedRounds=3");
for (int i = 0; i < 8; i++) {
LONG_LIVED.add(new byte[512 * 1024]);
}
System.out.println("longLivedCount=" + LONG_LIVED.size());
System.out.println("demoFinished=true");
}
private static void allocateShortLived() {
for (int i = 0; i < 3000; i++) {
byte[] data = new byte[8 * 1024];
if (data.length == -1) {
System.out.println("never");
}
}
}
}7.2 编译并运行
先编译:
bash
javac GcAlgorithmDemo.java再用小堆和 GC 日志运行:
bash
java -Xms32m -Xmx32m -Xlog:gc* GcAlgorithmDemo7.3 你应该观察到什么
程序输出应包含这些关键信息:
text
weakBeforeClear=true
weakAfterGc=true
shortLivedRounds=3
longLivedCount=8
demoFinished=true同时 GC 日志中通常还能观察到类似信息:
text
Pause Young
Heap具体格式和收集器有关,但你通常能观察到:
- 短命对象会持续推动年轻代回收
- 被长期持有的对象不会因为短期 GC 立刻消失
7.4 每一行在验证什么
weakBeforeClear=true:说明对象一开始仍然可达weakAfterGc=true:说明强引用断开后,对象在 GC 后可被回收shortLivedRounds=3:说明程序确实制造了大量短命对象longLivedCount=8:说明有一批对象被长期持有,不会轻易被回收- GC 日志中的年轻代停顿信息:说明对象分布特征会直接影响回收行为
7.5 再做两个延伸验证
你可以继续做下面两个实验:
- 把
LONG_LIVED的对象数量继续加大 - 把短命对象分配循环次数再提高
你可以观察:
- 长寿命对象增多会提高整体内存占用
- 短命对象增多会放大年轻代 GC 压力
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 画出标记-清除、标记-整理、复制算法示意图
- 总结每种算法的优缺点
- 用自己的话解释为什么复制算法适合新生代
- 结合对象生命周期思考为什么 JVM 常做分代回收
9. 自测问题
- JVM 如何判断对象是否可以回收?
- 标记-清除和标记-整理的核心差别是什么?
- 复制算法为什么适合新生代?
- 什么是内存碎片?
- 分代回收思想解决了什么问题?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
- JVM 主要通过可达性分析判断对象是否存活
- 标记-清除会带来碎片,标记-整理会通过移动对象降低碎片
- 复制算法在存活对象少时回收效率高
- GC 算法需要在停顿、吞吐、空间利用率之间权衡
- 分代思想是基于对象生命周期分布做出的工程优化