JVM主要通过可达性分析算法(Reachability Analysis)来判断对象是否存活,同时也支持引用计数算法作为辅助或特定场景下的实现。以下是核心机制详解:
一、可达性分析算法(主流方案)
这是JVM(如HotSpot)实际采用的算法,通过判断对象是否与GC Roots存在引用链来确定对象存活。
1. GC Roots 根对象集合
以下对象可作为GC Roots:
- 虚拟机栈中的引用
(例如:局部变量表、方法参数) - 方法区中静态属性引用
(例如:类的静态变量) - 方法区中常量引用
(例如:字符串常量池的引用) - 本地方法栈中JNI引用
(Native方法引用的对象) - Java虚拟机内部引用
(例如:系统类加载器、基本数据类型对应的Class对象) - 被同步锁持有的对象
(synchronized锁对象) - 活跃线程对象
2. 标记过程
- 从所有GC Roots出发,遍历整个引用链。
- 可达对象:被标记为存活。
- 不可达对象:标记为可回收(但不会立即回收)。
二、引用类型与回收策略
Java提供4种引用类型,影响对象生命周期:
| 引用类型 | 特点 | 回收时机 |
|---|---|---|
| 强引用 | Object obj = new Object() |
永远不回收(除非不可达) |
| 软引用 | SoftReference<T> |
内存不足时回收 |
| 弱引用 | WeakReference<T> |
下次GC必回收 |
| 虚引用 | PhantomReference<T> |
不影响生命周期,回收时收到通知 |
三、对象"死亡"的两次标记过程
即使对象不可达,也需经过以下流程:
1. 第一次标记
- 对象在可达性分析后不可达,被标记为可回收。
- 若对象未覆盖
finalize()或已被调用过,则直接进入回收队列。
2. 第二次标记(finalize机制)
- 若对象覆盖了
finalize()且未被调用过,则将其放入 F-Queue 队列。 - Finalizer线程 异步执行
finalize()方法。 - 对象可在
finalize()中重新与引用链关联 (例如:将this赋值给某个静态变量),从而逃脱回收。 - 若执行后仍不可达,则真正被回收。
⚠️ 注意 :
finalize()不稳定且性能差,Java 9+已弃用,不推荐依赖该方法进行资源释放。
四、方法区的回收条件
方法区(元空间)主要回收:
- 废弃的常量(例如:字符串常量池中无引用的字符串)。
- 不再使用的类 (需同时满足以下条件):
- 该类所有实例已被回收。
- 加载该类的
ClassLoader已被回收。 - 该类对应的
java.lang.Class对象无任何引用。
五、其他判断机制
1. 引用计数算法(非主流)
- 为每个对象维护引用计数器,引用增减时更新。
- 缺点 :无法解决循环引用问题(如两个对象互相引用但不被GC Roots引用)。
- JVM主流实现未采用此算法,但某些场景(如Python、Objective-C)使用。
2. 卡表(Card Table)与跨代引用
- 为解决跨代引用(如老年代对象引用新生代对象)的全堆扫描问题,使用卡表记录跨代引用指针。
- 加速年轻代GC时对老年代引用的扫描。
六、总结与最佳实践
| 场景 | 推荐策略 |
|---|---|
| 对象生命周期管理 | 使用软引用/弱引用代替强引用(如缓存场景) |
| 资源释放 | 使用 try-with-resources 或显式调用 close(),避免依赖 finalize() |
| 循环引用 | JVM的可达性分析自动处理,无需开发者干预 |
| 大对象监控 | 结合GC日志分析对象存活时间,优化内存分配 |
关键结论 :
JVM通过GC Roots的可达性分析 作为核心判断机制,配合分代收集 和引用类型,实现高效的内存回收。开发途中应理解对象引用链的构建,避免无意识的对象持有(如集合缓存泄漏、匿名内部类持有外部引用等)。