真实面经题目 · 原创解析
JVM 如何判断对象可以被回收,分代收集如何工作?
这道题核心不是背垃圾收集器名称,而是说明 JVM 如何从“对象是否还可被程序使用”推导出“是否可回收”,再解释为什么堆会按年轻代和老年代组织。回答时要把可达性分析、GC Roots、引用强度、finalize 的特殊边界,以及 Minor GC、Major GC、Full GC、对象晋升和跨代引用处理串成一条完整链路。
真实面经题目 · 原创解析
这道题核心不是背垃圾收集器名称,而是说明 JVM 如何从“对象是否还可被程序使用”推导出“是否可回收”,再解释为什么堆会按年轻代和老年代组织。回答时要把可达性分析、GC Roots、引用强度、finalize 的特殊边界,以及 Minor GC、Major GC、Full GC、对象晋升和跨代引用处理串成一条完整链路。
JVM 判断对象是否可以回收,主流方式是可达性分析:从一组 GC Roots 作为起点,沿着对象引用关系向外遍历,能被访问到的对象认为仍然存活,不能从任何 GC Roots 到达的对象就是不可达对象,通常具备被回收的条件。GC Roots 常见包括虚拟机栈中局部变量引用的对象、方法区中类静态属性引用的对象、常量引用的对象、JNI 本地方法引用的对象、正在运行线程相关对象等。这里还要结合引用类型理解,强引用存在时对象不会被回收,软引用通常在内存紧张时回收,弱引用只要发生 GC 就可能被清理,虚引用主要用于跟踪回收通知。finalize 不是可靠的保活机制,不可达对象最多只有一次被重新救活的机会,实际开发不应依赖它。分代收集基于分代假说:大多数对象朝生夕死,少数对象会长期存活,所以新对象通常进入年轻代,Minor GC 高频处理年轻代;多次存活或放不下的对象会晋升到老年代,老年代回收频率更低、成本更高。Major GC 通常指老年代回收,Full GC 通常涉及整个堆甚至类元数据等更大范围。为了避免每次 Minor GC 都扫描整个老年代,JVM 通过写屏障维护记忆集,记录老年代到年轻代的跨代引用,从而在年轻代回收时仍能保证可达性判断正确。
JVM 不是简单看一个对象有没有被其他对象引用,而是从 GC Roots 出发做可达性分析。只要某个对象能从根对象经过一条或多条引用链访问到,它就被认为仍然可能被程序使用,不能回收。反过来,一个对象即使内部互相引用,只要整个引用环无法从 GC Roots 到达,也会被判定为不可达,这也是它相比引用计数法更能处理循环引用的关键。
GC Roots 可以理解为当前运行时一定要保守认为存活的引用起点。常见来源包括线程栈帧里的局部变量和操作数栈引用、类的静态字段引用、运行时常量池或字符串常量相关引用、JNI 本地方法持有的引用、正在运行线程和同步锁相关对象等。面试中要注意,GC Roots 不是固定某几个对象,而是一类由虚拟机运行状态决定的根集合。
可达性分析还要结合 Java 的引用强度理解。强引用最常见,只要引用链上存在强引用,对象通常不会被回收;软引用适合缓存,内存紧张时可能被回收;弱引用强度更低,只要发生垃圾回收,弱引用关联对象就可能被清理;虚引用不能用来取得对象,主要配合引用队列感知对象回收。引用类型不改变可达性分析的框架,但会影响对象在不同压力和阶段下的处理策略。
不可达对象并不一定立刻被释放,历史上还存在 finalize 相关的二次标记过程。如果对象覆盖了 finalize 且还没有执行过,虚拟机可能把它放入队列等待执行,执行期间对象理论上可以把自己重新赋给某个可达引用,从而获得一次自救机会。但 finalize 执行时机不确定、只会被系统尝试一次、还可能拖慢回收流程,因此不能作为资源释放或对象保活的正常手段。
分代收集的基础是假设大部分对象生命周期很短,少数对象会存活很久。为了利用这个规律,堆通常被划分为年轻代和老年代,新创建的普通对象优先进入年轻代。年轻代对象死亡率高,回收时只需要处理较小区域,就能获得大量可用空间,所以 Minor GC 可以更频繁地发生。老年代保存经过多轮筛选仍存活的对象,回收频率较低,但单次成本通常更高。
年轻代通常包含 Eden 和两个 Survivor 区。对象大多先进入 Eden,Minor GC 时存活对象会被复制到 Survivor 区,并记录对象年龄;对象每熬过一次年轻代回收,年龄可能增加。达到年龄阈值、Survivor 空间不足、同龄对象累计大小超过动态判定条件,或者对象本身较大时,都可能进入老年代。对象晋升不是单一规则,而是年龄、空间和收集器策略共同决定的结果。
Minor GC 通常指年轻代回收,触发频繁但单次停顿相对可控。Major GC 一般用来指老年代回收,不过不同资料和收集器语境下说法可能有差异。Full GC 范围更大,通常会处理整个 Java 堆,并可能涉及方法区或元空间、类卸载、直接内存触发链路等,因此停顿风险更高。面试回答中最好说明这些术语的常见含义,同时承认具体行为要看收集器实现。
年轻代回收不能只扫描年轻代内部引用,因为老年代对象也可能引用年轻代对象。如果每次 Minor GC 都完整扫描老年代,就会破坏分代收集的性能优势。写屏障可以在引用赋值时捕获跨代引用变化,并把相关信息记录到记忆集或卡表中。这样年轻代 GC 只需要把这些记录区域作为额外根来源,就能找到被老年代引用的年轻对象,兼顾正确性和效率。
引用计数法思路简单,每有一个引用就加一,引用失效就减一,但它很难自然处理循环引用。两个对象互相引用时计数都不为零,即使它们已经无法从任何根对象访问到,也可能无法释放。可达性分析从 GC Roots 出发,可以识别这种不可达的引用环。
强引用最稳定,只要强引用链可达,对象通常不会被回收。软引用常用于缓存,内存不足时可能被清理。弱引用只要发生 GC 就可能被回收,适合非强拥有关系。虚引用不能取得对象本身,主要用于对象回收后的通知和资源清理协作。
Minor GC 通常回收年轻代,触发频繁,目标是快速清理大量短命对象。Major GC 常被用来指老年代回收,但不同语境会有差异。Full GC 范围通常更大,可能覆盖整个堆以及类元数据相关区域,停顿风险和排查优先级一般更高。
对象在年轻代多次 Minor GC 后仍存活,年龄达到阈值时可能晋升;如果 Survivor 区放不下,部分对象也会提前进入老年代;大对象可能直接分配到老年代。实际规则还会受到动态年龄判断、空间担保和具体收集器实现影响。
因为老年代对象可能引用年轻代对象,年轻代回收时必须把这些被引用对象视为存活。如果每次都扫描整个老年代,Minor GC 成本会很高。写屏障在引用写入时记录跨代引用,记忆集保存这些线索,让回收器只扫描必要区域。
理论上,不可达对象在 finalize 执行期间可能把自己重新关联到可达对象上,完成一次自救。但 finalize 执行时机不可控、只会被尝试一次、还会增加回收复杂度和性能风险,因此不能作为可靠的生命周期管理方式,实际开发应避免依赖它。