JVM判定对象能否回收的唯一答案是可达性分析法:从GC Roots(虚拟机栈局部变量、方法区静态/常量引用、本地方法栈JNI引用)出发,不可达的对象即为垃圾;引用计数法因无法解决循环引用且开销大,被JVM弃用。

直接从“对象能不能被回收”这个最底层问题切入,是最高效的学习路径。别一上来就背收集器名字或调参数,先搞懂 JVM 是怎么一眼认出哪个对象该清理的——这一步没理清,后面全在空中建楼。
先死磕两个判定逻辑:为什么引用计数不用,而可达性分析是唯一答案
引用计数法听着简单:谁用它就+1,不用就-1,归零就回收。但它卡在循环引用上——A 持有 B,B 持有 A,外部都没人用了,计数器却都不为 0。JVM 不采用它,不是因为懒,而是因为真实业务里这种结构太常见(比如监听器、树形节点、缓存包装类)。
可达性分析才是正解:从 GC Roots 出发,沿着引用链往下找。只要某对象跟 Roots 断了联系,哪怕它自己内部还连着一堆对象,也立刻判“死亡”。GC Roots 就四类:
- 虚拟机栈里正在用的局部变量(比如方法里 new 出来还没 return 的对象)
- 方法区里的静态变量(public static final Map
CACHE 这种) - 方法区里的常量(字符串常量池里的字面量)
- 本地方法栈里 JNI 调用中还在使用的 Java 对象
动手验证很简单:写个含循环引用的类,把两边变量设为 null 后触发一次 System.gc()(仅用于学习观察),再用 jstat 或 VisualVM 看堆内存变化——你会发现对象真被收走了。这比看十页理论更管用。
立即学习“Java免费学习笔记(深入)”;
分代模型不是玄学,是基于真实对象生命周期统计出来的规律
你写的大部分对象,比如一次 HTTP 请求里生成的 DTO、临时 StringBuilder、JSON 解析中间对象,活不过几次方法调用。而像连接池、配置容器、Spring 容器本身,一启动就存在,几乎不销毁。JVM 把堆分成新生代(Eden + 两个 Survivor)和老年代,就是照着这个“绝大多数对象朝生暮死,极少数长命百岁”的现实设计的。
所以不用硬记“新生代用复制算法,老年代用标记整理”,去想场景:
- Eden 区满了 → 触发 Minor GC:活着的对象抄到一个 Survivor,年龄+1;熬过几次复制的,推进老年代
- 老年代快满了 → 触发 Major GC(或 Full GC):标记所有还活着的,然后压缩整理,腾出连续空间
- 如果一次 Minor GC 发现老年代没空间容纳晋升对象,可能直接升级成 Full GC
用 -XX:+PrintGCDetails 跑个小 demo,看日志里 “PSYoungGen” 和 “ParOldGen” 的变化,比画一百张示意图都直观。
收集器选型不靠背诵,靠压测时看三样东西:停顿时间、吞吐量、内存占用
Serial 适合单核小应用,Parallel 注重吞吐(适合后台批处理),CMS 曾经主打低延迟但已被淘汰,G1 是目前通用主力,ZGC/Shenandoah 面向超低延迟(毫秒级 STW)。但选哪个,不能光听宣传。
真实判断方法:
- 你的服务对响应时间敏感吗?比如网关、实时推荐,那就优先试 G1 或 ZGC,开 -Xlog:gc* 看 STW 是否稳定在 10ms 内
- 你的任务是夜间跑报表,几秒停顿无所谓,但要尽快算完?Parallel 可能反而更快
- 堆设了 32GB,但老年代才占 2GB,频繁 CMS 回收失败?大概率是内存分配不合理,不是换收集器能解决的
调参不是调数字游戏。比如 -XX:MaxGCPauseMillis=200 告诉 G1 “尽量控制单次停顿在 200ms 内”,但它会自动调小堆、增加 GC 频率来满足——结果可能是 GC 次数翻倍,总耗时反而上升。得结合监控看效果。
调优不是终点,是带着 GC 日志反推代码问题的起点
线上出现频繁 GC,第一反应不该是改 JVM 参数,而是查日志里“promotion failed”“concurrent mode failure”这类关键词:
- 频繁 Minor GC 且 Survivor 区总装不下?说明对象存活率高,可能是缓存没设上限、日志对象创建太猛
- 老年代增长飞快?检查有没有大对象直接进老年代(-XX:PretenureSizeThreshold 设置不当)、或集合类长期持有短命对象引用
- Full GC 后老年代释放很少?大概率有内存泄漏,用 jmap -histo 或 MAT 分析堆转储,重点看 HashMap、ConcurrentHashMap、ThreadLocalMap 里堆积的实例
真正熟练的标志,不是你会配 G1 的 Region 大小,而是看到 GC 日志,能猜出哪段代码在“作妖”。学的时候就该拿自己项目里的一个接口,压测→抓日志→定位→改代码→再压测,闭环走三遍,比读十篇原理强得多。


















