news 2026/4/26 22:18:58

java面试必问24:Java 垃圾回收机制:从对象判死到分代回收,一篇讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
java面试必问24:Java 垃圾回收机制:从对象判死到分代回收,一篇讲透

Java 垃圾回收机制:从对象判死到分代回收,一篇讲透

面试官:“Java 如何判断一个对象可以被回收?”
你:“两种方式:引用计数法和可达性分析。主流 JVM 使用可达性分析,从 GC Roots 出发,不可达的对象就是垃圾。”
面试官:“那 GC Roots 包括哪些?引用计数法有什么缺陷?”
你:“……”

很多人能背出“可达性分析”,但一问到具体算法、分代回收原理、GC Roots 细节就卡壳了。本文从对象死亡判定到垃圾回收算法,结合 HotSpot 实现,彻底讲透 Java 垃圾回收机制。


一、垃圾回收(GC)是什么?

垃圾回收是指自动管理内存的一种机制:自动识别并回收不再使用的对象,释放内存空间。Java 程序员不需要手动freedelete,JVM 会帮我们完成这项工作。垃圾回收的核心问题有两个:

  1. 哪些内存需要回收?—— 对象死亡判定
  2. 如何回收?—— 垃圾回收算法

二、判断对象死亡的两种方式

1. 引用计数法(Reference Counting)

原理:每个对象维护一个引用计数器,每当有一个地方引用它,计数器 +1;引用失效时,计数器 -1。计数器为 0 的对象就是垃圾。

优点:简单高效,实时性好,可以立即回收垃圾。

缺点(致命缺陷)

  • 循环引用:对象 A 引用 B,B 引用 A,除此之外无其他引用。两者计数器都不为 0,永远不会被回收,导致内存泄漏。
  • 需要额外空间存储计数器,且加减操作频繁。

应用:Python、PHP 等语言早期使用,但都搭配了其他算法解决循环引用。JVM 没有采用引用计数法

2. 可达性分析(Reachability Analysis)

原理:从一组称为GC Roots的根对象出发,通过引用链向下搜索,形成引用网络。在这个网络中的对象是“可达”的(存活),不在网络中的对象就是“不可达”的(可回收)。

优点:天然解决循环引用问题——A 和 B 互相引用,但如果它们没有任何 GC Roots 引用,就会被判定为不可达,一起回收。

GC Roots 包括

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中静态属性引用的对象(如static字段)
  • 方法区中常量引用的对象(如final常量)
  • 本地方法栈中 JNI 引用的对象(Native 方法)
  • Java 虚拟机内部的引用(基本类型的 Class 对象、常驻异常对象等)
  • 所有被同步锁(synchronized)持有的对象
  • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 回调等

注意:即使对象被可达性分析判定为不可达,也不一定会立即回收。它至少需要经过两次标记过程(第一次标记并筛选是否执行finalize(),第二次标记真正回收)。finalize()方法已被 JDK 9 标记为废弃,不推荐使用。

结论HotSpot JVM 采用可达性分析作为对象存活判定算法


三、常见垃圾回收算法

有了“哪些是垃圾”,接下来就是“如何回收垃圾”。垃圾回收算法是垃圾收集器实现的理论基础。

1. 标记-清除(Mark-Sweep)

过程

  1. 标记:遍历所有 GC Roots,标记出所有存活对象。
  2. 清除:线性遍历堆内存,回收所有未被标记的对象的内存。

图示

初始: [A][B][C][D][E] (A、C、E 存活) 标记后: [✓][×][✓][×][✓] (✓存活,×垃圾) 清除后: [A][ ][C][ ][E] (内存碎片)

优点:简单,不需要移动对象。

缺点

  • 效率问题:标记和清除都需要遍历,对象越多越慢。
  • 空间碎片:清除后产生大量不连续内存碎片,导致大对象无法分配,提前触发另一次 GC。

应用场景:CMS 收集器的“并发清除”阶段使用了标记-清除算法的变种。

2. 复制算法(Copying)

过程:将内存分为大小相等的两块(From 和 To),每次只使用其中一块。当这一块用满时,将存活对象复制到另一块,然后一次性清空当前块。

图示

初始: From [A][B][C][D] To [空] 存活 A、C → 复制到 To From 清空: From [空] To [A][C] 交换角色: From [A][C] To [空]

优点

  • 实现简单,运行高效(只需移动指针,无需处理碎片)。
  • 无内存碎片,分配新对象时只需指针碰撞(bump-the-pointer)。

缺点

  • 内存利用率低,只能用一半内存。
  • 对象存活率高时,复制开销大。

应用新生代 GC(如 Serial、ParNew、Parallel Scavenge),因为新生代对象存活率低,适合复制算法。HotSpot 将新生代分为 Eden + Survivor(两块,通常是 8:1:1),而不是 1:1,提高了内存利用率。

3. 标记-整理(Mark-Compact)

过程

  1. 标记:与标记-清除相同,标记存活对象。
  2. 整理:将所有存活对象向一端移动,然后直接清理边界以外的内存。

图示

标记后: [A][×][C][×][E] (A、C、E 存活) 整理后: [A][C][E][ ][ ] (存活对象靠左,右边空闲连续)

优点:无内存碎片,适合老年代(对象存活率高)。

缺点:移动对象需要更新所有引用地址,成本比标记-清除高。

应用老年代 GC(如 Serial Old、Parallel Old)

4. 分代回收(Generational Collection)

核心思想:根据对象存活周期的不同,将堆内存划分为不同区域,采用最适合的回收算法。

HotSpot 堆划分

  • 新生代(Young Generation):对象存活率低,使用复制算法。分为 Eden 区和两块 Survivor 区(From 和 To),默认比例 8:1:1。
  • 老年代(Old Generation):对象存活率高,使用标记-清除标记-整理算法(取决于收集器)。

为什么分代有效?

  • 大部分对象朝生夕灭(如局部变量、临时对象)。
  • 老年代对象生命周期长,回收频率低。
  • 分代可以针对不同区域采用不同策略,提升整体效率。

对象晋升(Promotion)

  • 新生代经过多次 GC 仍存活的对象,会晋升到老年代。
  • 大对象(超过阈值)直接分配到老年代。

卡表(Card Table):用于记录老年代对新生代的引用,避免每次 Young GC 都扫描整个老年代。


四、垃圾回收算法对比

算法内存利用率碎片问题移动对象适用场景
标记-清除100%老年代(CMS)
复制50%(或更高,如 90% 通过 Survivor)新生代
标记-整理100%老年代
分代回收高(组合使用)基本无部分综合

五、常见面试追问

Q1:引用计数法为什么不用于 JVM?

循环引用无法解决,例如 A 引用 B,B 引用 A,且外部没有引用,计数器永远不为 0,内存泄漏。

Q2:可达性分析中的 GC Roots 包括哪些?

见上文列表。尤其注意:被synchronized持有的对象、JNI 全局引用、活跃线程等也是 GC Roots。

Q3:标记-清除和标记-整理的区别?

标记-清除不移动对象,产生碎片;标记-整理移动对象,消除碎片,但成本更高。

Q4:为什么新生代使用复制算法?

新生代对象存活率低,每次回收只有少量对象存活,复制开销小,且无碎片。通过 Survivor 分区(8:1:1),内存浪费仅 10%,可接受。

Q5:直接调用System.gc()会发生什么?

会建议 JVM 执行 Full GC,但 JVM 不保证立即执行。频繁调用会严重影响性能,应避免。

Q6:finalize()方法的作用和问题?

对象被回收前,如果覆盖了finalize(),会被调用(仅一次)。但该方法执行时间不确定,且可能“复活”对象,已被 JDK 9 标记为废弃。推荐使用try-with-resourcesCleaner


六、总结

判定方式核心优缺点
引用计数法计数器简单,但无法解决循环引用
可达性分析GC Roots + 引用链无循环引用问题,主流方案
回收算法核心适用区域
标记-清除标记 + 清除老年代(CMS)
复制分半复制新生代
标记-整理标记 + 移动老年代(Serial Old、Parallel Old)
分代回收组合策略整个堆

一句话记住垃圾回收可达分析判生死,分代回收用对法;新生复制老整理,标记清除防碎片

垃圾回收算法是理解 JVM 调优和选择垃圾收集器的基础。掌握这些原理,才能看懂 GC 日志、优化停顿时间、排查内存问题。

希望这篇文章能帮你彻底掌握 Java 垃圾回收机制,欢迎继续讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/26 22:15:57

Kubernetes监控基石:kube-state-metrics核心原理与生产实践指南

1. 项目概述:为什么我们需要一个“集群状态翻译官”?在Kubernetes的日常运维和监控体系中,我们经常听到一个名字:kube-state-metrics。乍一看,它像是kubelet暴露的cAdvisor指标的一个补充,但如果你仅仅把它…

作者头像 李华
网站建设 2026/4/26 22:15:56

Lombok 注解教程

一、Lombok 简介Lombok 是一个 Java 库&#xff0c;通过注解自动生成常见代码&#xff08;getter/setter、构造方法、equals/hashCode 等&#xff09;&#xff0c;减少样板代码。安装&#xff1a;IDE 需安装 Lombok 插件Maven 依赖&#xff1a;<dependency><groupId&g…

作者头像 李华
网站建设 2026/4/26 21:55:50

如何快速诊断GPU内存故障:MemtestCL完整指南

如何快速诊断GPU内存故障&#xff1a;MemtestCL完整指南 【免费下载链接】memtestCL OpenCL memory tester for GPUs 项目地址: https://gitcode.com/gh_mirrors/me/memtestCL 还在为显卡频繁崩溃而烦恼吗&#xff1f;每次运行大型游戏或专业软件时&#xff0c;系统突然…

作者头像 李华
网站建设 2026/4/26 21:54:45

cursor的MCP怎么配置使用?

1.需要用nxp 安装drawio{"mcpServers": {"drawio": {"command": "npx","args": ["-y", "next-ai-drawio/mcp-serverlatest"]}} }怎么用 Cursor 里的 drawio MCP 画图&#xff08;含提示词示例&#xff…

作者头像 李华