JVM虚拟机:垃圾收集器和判断对象是否存活的算法

1. 为什么要了解垃圾收集?

虽然 Java 的内存动态分配和回收看起来是自动化的,但在以下场景中,我们需要深入了解其原理以进行必要的监控和调节:

  • 排查故障: 当出现内存溢出(OOM)、内存泄漏时。

  • 性能调优: 当 GC 成为系统达到更高并发量的瓶颈时。

内存回收的"主战场"

Java 运行时区域分为两类,GC 主要关注的是动态分配的区域:

区域 特性 是否需要关注 GC 原因
程序计数器、虚拟机栈、本地方法栈 随线程生灭 ❌ 不需要 栈帧出栈或线程结束时内存自然回收,具备确定性。
Java 堆、方法区 动态分配 重点关注 只有在运行时才知道会创建哪些对象、多少对象。

2. 判断对象是否存活的算法

在对堆进行回收前,必须确定哪些对象还"活着",哪些已经"死去"。

A. 引用计数算法 (Reference Counting)
  • 原理: 对象添加计数器,被引用+1,引用失效-1,为0则可回收。

  • 优点: 简单、效率高。

  • 缺点: 难以解决对象间循环引用的问题(如 A 引用 B,B 引用 A,除此之外无其他引用,导致无法回收)。

  • 现状: 主流 Java 虚拟机未采用此算法。

B. 可达性分析算法 (Reachability Analysis)

这是主流商用语言(Java, C#)采用的算法。

  • 原理: 从一组称为 "GC Roots" 的根对象出发,向下搜索。如果一个对象到 GC Roots 没有任何引用链 (Reference Chain) 相连,则证明该对象不可用。

  • 常见的 GC Roots 对象:

    1. 虚拟机栈(局部变量表)中引用的对象。

    2. 方法区中类静态属性引用的对象。

    3. 方法区中常量引用的对象(如字符串常量池)。

    4. 本地方法栈中 JNI(Native 方法)引用的对象。

    5. Java 虚拟机内部引用(如基本数据类型对应的 Class 对象、异常对象)。

    6. 被同步锁 (synchronized) 持有的对象。


3. Java 的四种引用强度 (JDK 1.2+)

判定对象存活与"引用"密切相关。Java 将引用分为四种,强度由高到低:

引用类型 描述 回收时机 用途示例
强引用 (Strong) 最普遍的引用 (如 new Object())。 永不回收(只要引用关系还在)。 普通对象实例化。
软引用 (Soft) 还有用但非必须的对象。 内存溢出前进行二次回收。 缓存功能。
弱引用 (Weak) 非必须对象,强度比软引用更弱。 下一次 GC 时无论内存是否足够必回收。 WeakHashMap,缓存。
虚引用 (Phantom) 最弱,无法通过它获取对象实例。 无影响,回收时收到系统通知。 跟踪对象回收状态,资源清理。

4. 对象死亡的"缓刑"与自救

即使在可达性分析中不可达,对象也不一定会立即死亡,它有一次自我拯救的机会。

  1. 第一次标记: 对象不可达,进行筛选。如果对象覆盖了 finalize() 方法且未被执行过,则放入 F-Queue 队列。

  2. 执行 finalize(): 虚拟机会触发该方法(但不保证等待其运行结束)。

  3. 自我拯救:finalize() 中将自己(this)赋值给某个类变量或成员变量,即可逃脱回收。

  4. 第二次标记: 如果没逃脱,则被回收。

⚠️ 重要提示: finalize() 运行代价高、不确定性大且只能执行一次。官方明确不推荐使用 ,建议使用 try-finally 块替代资源关闭工作。


5. 方法区的回收

方法区(如永久代或元空间)的回收条件非常苛刻,主要回收两部分:

  1. 废弃的常量: 如没有任何对象引用的字符串常量。

  2. 不再使用的类型 (Class): 必须同时满足以下三个条件:

    • 该类所有实例都已被回收。

    • 加载该类的 ClassLoader 已被回收。

    • 该类对应的 java.lang.Class 对象没有在任何地方被引用(无法通过反射访问)。

这种回收常见于大量使用反射、动态代理、OSGi 等频繁自定义类加载器的场景。

核心知识点问答

1. 问题:为什么 Java 内存管理已经自动化了,我们还需要了解垃圾收集(GC)?

解答: 虽然自动化了,但在以下情况我们需要实施监控和调节:

  1. 需要排查各种内存溢出(OOM)、内存泄漏问题时。

  2. 当垃圾收集成为系统达到更高并发量的性能瓶颈时。

2. 问题:Java 运行时内存区域中,哪些是垃圾收集器关注的重点?为什么?

解答: GC 主要关注 Java 堆(Java Heap)和方法区(Method Area)

  • 原因 :程序计数器、虚拟机栈、本地方法栈这三个区域随线程而生灭,内存分配和回收具备确定性(栈帧大小编译期基本可知),方法结束或线程结束时内存自然回收。而 Java 堆和方法区的内存分配在运行期间是具有显著不确定性的(例如不同条件分支创建的对象数量不同),GC 关注的正是这部分动态内存的管理。

3. 问题:判断对象是否存活的"引用计数算法"有什么优缺点?为什么主流 Java 虚拟机不使用它? 解答:

  • 原理:在对象中添加引用计数器,引用加一,失效减一,计数为零则不可用。

  • 优点:原理简单,判定效率高。

  • 缺点(核心原因) :很难解决对象之间相互循环引用的问题。如果两个对象互相引用但不再被其他地方引用,它们的计数都不为零,导致无法回收。

4. 问题:主流商用程序语言(如 Java)目前使用什么算法判定对象是否存活?基本思路是什么? 解答: 使用的是可达性分析算法(Reachability Analysis)

  • 基本思路 :通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,走过的路径称为"引用链"。如果某个对象到 GC Roots 间没有任何引用链相连(即不可达),则证明此对象不再被使用。

5. 问题:在 Java 中,哪些对象可以固定作为 GC Roots?

解答:

  1. 虚拟机栈(栈帧中的局部变量表)中引用的对象。

  2. 方法区中类静态属性引用的对象。

  3. 方法区中常量引用的对象(如字符串常量池)。

  4. 本地方法栈中 JNI(Native 方法)引用的对象。

  5. Java 虚拟机内部的引用(如基本类型的 Class 对象、常驻异常对象、系统类加载器)。

  6. 所有被同步锁(synchronized)持有的对象。

  7. 反映 JVM 内部情况的 JMXBean 等引用的对象。

6. 问题:JDK 1.2 之后,Java 对"引用"的概念进行了哪些扩充?它们的区别是什么?

解答: Java 将引用分为 4 种强度(由强到弱):

  1. 强引用 (Strong Reference) :最传统的引用(如 new Object()),只要存在,GC 永远不会回收。

  2. 软引用 (Soft Reference):描述还有用但非必须的对象。在系统将要发生内存溢出异常前,会列入回收范围进行二次回收。用于缓存。

  3. 弱引用 (Weak Reference):非必须对象,只能生存到下一次 GC 发生之前。无论内存是否足够都会被回收。

  4. 虚引用 (Phantom Reference):最弱的引用。完全不影响对象生存时间,无法通过它获取对象实例。唯一目的是在对象被回收时收到系统通知。

7. 问题:一个对象在可达性分析中被判定不可达后,就立即死亡了吗?它还有机会自救吗?

解答: 不是立即死亡,处于"缓刑"阶段。要宣告死亡至少经历两次标记过程。 对象机会自救,但只有一次机会:

  • 如果对象覆盖了 finalize() 方法且尚未被执行,它会被放入 F-Queue 队列等待执行。

  • finalize() 方法中,如果对象重新与引用链上的任意对象建立了关联(譬如把自己赋值给某个类变量),它在第二次标记时就会被移出"即将回收"的集合,实现自救。

  • 注意 :任何一个对象的 finalize() 方法最多只会被系统自动调用一次。官方不推荐使用该方法。

8. 问题:方法区(如元空间)需要垃圾收集吗?回收的条件是什么?

解答: 方法区垃圾收集行为,但性价比通常较低。主要回收两部分内容:

  1. 废弃的常量:没有对象引用该常量且虚拟机中无其他地方引用。

  2. 不再使用的类型(类):判定条件非常苛刻,需同时满足三点:

    • 该类的所有实例都已经被回收。

    • 加载该类的类加载器已经被回收。

    • 该类对应的 java.lang.Class 对象没有在任何地方被引用(无法通过反射访问)。

finalize() 方法

是 Java 中一个特殊的方法,它是在对象被垃圾收集器回收之前,由虚拟机自动调用的一个方法。

以下是关于 finalize() 方法的一些关键点:

  • 拯救对象的机会 :它是对象逃脱死亡命运的最后一次机会。如果一个对象在可达性分析中被判定为不可达,它会被第一次标记。如果该对象覆盖了 finalize() 方法且该方法尚未被虚拟机调用过,那么这个对象会被放入一个 F-Queue 队列中。稍后,虚拟机会建立一个低优先级的 Finalizer 线程去触发这个方法。

  • 自我拯救 :在 finalize() 方法中,对象可以通过重新与引用链上的任何一个对象建立关联(例如把 this 关键字赋值给某个类变量或对象的成员变量)来拯救自己。如果拯救成功,在第二次标记时它将被移出"即将回收"的集合。

  • 只调用一次 :任何一个对象的 finalize() 方法都只会被系统自动调用一次。如果对象在 finalize() 中拯救了自己,但之后再次面临回收,它的 finalize() 方法将不会被再次执行。

  • 不确定性 :虚拟机虽然会触发 finalize() 方法的执行,但并不承诺会等待它运行结束。这是为了防止某个对象的 finalize() 方法执行缓慢或发生死循环,从而导致 F-Queue 队列阻塞,甚至拖垮整个内存回收子系统。

  • 不推荐使用finalize() 方法的运行代价高昂,不确定性大,且无法保证各个对象的调用顺序。它已被官方明确声明为不推荐使用的语法。建议开发者尽量避免使用它,可以使用 try-finally 或其他方式来替代它进行资源清理工作。

相关推荐
我是苏苏2 小时前
C#高级:使用ConcurrentQueue做一个简易进程内通信的消息队列
java·windows·c#
ballball~~2 小时前
拉普拉斯金字塔
算法·机器学习
Cemtery1162 小时前
Day26 常见的降维算法
人工智能·python·算法·机器学习
heartbeat..3 小时前
数据库基础知识体系:概念、约束、范式与国产产品
java·数据库·学习笔记·国产数据库
PXM的算法星球4 小时前
【操作系统】哲学家就餐问题实现详解
java
2301_815357704 小时前
Java项目架构从单体架构到微服务架构的发展演变
java·微服务·架构
Ethan-D4 小时前
#每日一题19 回溯 + 全排列思想
java·开发语言·python·算法·leetcode
Benny_Tang4 小时前
题解:CF2164C Dungeon
c++·算法
仙俊红4 小时前
LeetCode174双周赛T3
数据结构·算法