先说一个很多开发者容易踩的坑:System.gc() 并不是什么“一键回收”开关,它充其量只是向 JVM 递了一张“建议现在可以清理一下”的便条。至于 JVM 会不会搭理、什么时候搭理、用哪种方式搭理——全看它自己的心情和策略。尤其是在现代 JDK 上(比如 JDK 17+ 默认 G1、JDK 21+ 默认 ZGC),这张便条经常被直接扔进碎纸机。

System.gc() 不触发回收,只发出建议。它既不决定 GC 类型,也不控制执行时机,更不会改变对象存活判定逻辑。JVM 是否真的执行回收、执行哪一类 GC(Minor、Mixed 还是 Full),完全由当前内存压力、GC 策略(如 G1、ZGC)、运行时统计信息等内部机制说了算。
它到底做了什么?
调用 System.gc() 本质上等价于 Runtime.getRuntime().gc(),底层向 JVM 发起一次“建议执行 Full GC”的信号。但现代 JVM 的做法越来越“任性”:
- G1 在大多数场景下直接忽略这个建议,除非你显式加了
-XX:+ExplicitGCInvokesConcurrent让它“变通”一下; - ZGC 和 Shenandoah 甚至根本不支持同步 Full GC,所以这个调用对它们来说等于不存在。
总结三句话:
- 不是命令,而是提示(hint),类似“可能现在适合打扫一下”;
- 是否响应、何时响应、以何种方式响应,全由 JVM 自主决策;
- 即使响应,也未必是 Full GC——某些配置下(比如开启
-XX:+ExplicitGCInvokesConcurrent),它可能只触发并发标记阶段,而不是完整的 Stop-The-World 回收。
弱引用回收与 System.gc() 没有因果关系
这一点尤其容易混淆。弱引用对象被回收的唯一条件是:在某次实际发生的 GC 中,该对象不可达(即仅被 WeakReference 持有,且没有强引用或软引用路径)。这跟是否调用 System.gc() 毫无关系。
- 没调用
System.gc(),JVM 照样会在 Eden 区满时触发 Minor GC,顺手就把弱引用对象给清了; - 调用了
System.gc(),但如果堆内存还很充裕、G1 认为没必要回收,那么弱引用对象依然活得好好的; - 哪怕实际发生了 GC,只要对象还被其他强引用或软引用路径持有,
WeakReference.get()依然返回非 null。
所以,拿 System.gc() 来测试弱引用行为,结果往往不可靠。
如何观察真实 GC 行为?
想搞清楚 GC 到底发生了什么,靠谱的做法是结合日志和代码控制,而不是靠 System.gc() 碰运气:
- 加上 JVM 参数:
-XX:+PrintGCDetails -Xlog:gc*:stdout:time(不同 JDK 版本参数略有差异,但思路一样); - 确保目标对象的所有强引用都已断开(比如把引用变量设为 null);
- 用
weakRef.get()轮询检测,而不是盲等固定时间——GC 是异步事件,睡多久都没准; - 配合
ReferenceQueue来监听弱引用入队时机,比轮询更及时也更准确。
为什么设计成“建议”而非“指令”?
假设 System.gc() 是一个强制指令,那麻烦就大了:
- 在高负载时段强行 Full GC,可能导致 STW 时间飙升,服务直接超时;
- 频繁调用会破坏分代假设,降低复制算法的效率;
- 现代 GC(如 ZGC)采用并发标记与重定位,根本无法按需“立刻停顿回收”。
说到底,System.gc() 是一个历史遗留接口,保留它只是为了向后兼容那些老旧的代码。生产环境里,还是尽量别碰它——把内存管理交给 JVM 自己,它比我们更清楚什么时候该动手。
