当前位置: 首页 > news >正文

JVM 中“对象存活判定方法”全面解析

1. 前言

在 Java 开发过程中,我们常常听到“垃圾回收”(Garbage Collection, GC)这一术语。JVM 通过垃圾回收机制自动管理内存,极大地简化了程序员的内存控制负担。然而,GC 究竟是如何判断哪些对象该回收、哪些应保留的呢?这正是“对象存活判定”的关键所在。

对象存活判定方法的效率和准确性,直接关系到系统性能的高低。在内存紧张的场景中,一个不合理的回收策略可能导致频繁 GC,甚至导致 OutOfMemoryError(OOM)错误。

Java 语言自诞生之初就非常重视内存安全问题,JVM 也随着版本更新不断优化垃圾回收算法。目前主流的 Java 8 就采用了更加高效的“可达性分析法”替代传统的“引用计数法”,以规避引用循环等典型问题。

本系列文章将从基础原理入手,深入剖析 JVM 如何判断对象是否“还活着”,并配合图解与示例代码,帮助开发者更好地理解这一 GC 背后的核心机制。

阅读后你将收获:

  • JVM 内存模型与对象管理原理

  • 引用计数法与可达性分析法的差异与优劣

  • 如何用代码分析对象是否会被 GC 回收

  • 实用工具(MAT、JVisualVM)的分析技巧

让我们从最基础的问题开始——“对象存活判定”到底指的是什么?

2. 什么是对象存活判定?

对象存活判定(Object Liveness Detection)是指 JVM 在垃圾回收过程中判断某个对象是否仍“有用”的一套逻辑机制。只有当 JVM 确认一个对象“无用”时,才会将其内存空间释放。

那么,什么是“有用”或“无用”?这个标准并非由程序员显式指定,而是 JVM 通过一定的算法推导得出。

从 JVM 的角度来看:

  • “有用”的对象:程序仍然可以访问到该对象。

  • “无用”的对象:程序中不再有任何方式可以访问到该对象。

这就涉及到两个关键问题:

  1. 程序如何“访问”一个对象?

  2. JVM 如何判断“是否还能访问”?

为了解决这两个问题,JVM 提供了两种主要的判断方式:

  • 引用计数法(Reference Counting):为每个对象维护一个引用计数器。

  • 可达性分析法(Reachability Analysis):从一组被称为 GC Roots 的起点出发,遍历对象图。

这两种方法各有利弊,也体现了 JVM 垃圾回收策略的演进方向。

在接下来的章节中,我们将逐一剖析这两种算法的底层原理、适用场景与实现机制,并结合示意图和代码说明其工作方式。

3. 方法一:引用计数法

基本原理

引用计数法的核心思想非常直观:

每当有一个地方引用该对象,其引用计数就加 1; 每当有一个引用失效,其引用计数就减 1; 当引用计数为 0 时,说明该对象“无人引用”,可以回收。

这一机制类似于手动管理内存语言(如 C++ 的智能指针),但在 Java 中并未采用这种方式作为主流实现。

示意图

假设我们有以下引用关系:

A --> B --> C^     ||_____|
  • 对象 A 引用了 B,B 引用了 C,C 又回头引用了 B,构成循环引用。

  • 即便 A 被回收,B 与 C 相互引用,导致引用计数不为 0,从而无法释放。

这就是引用计数法的致命缺陷:无法处理对象之间的循环引用问题

示例代码

虽然 Java 官方并未公开支持引用计数 GC,但我们可以通过伪代码演示其原理:

class MyObject {int refCount = 0;void addReference() {refCount++;}void removeReference() {refCount--;if (refCount == 0) {// 回收对象内存System.out.println("对象可以被回收");}}
}

使用时:

MyObject obj = new MyObject();
obj.addReference(); // 引用 +1
obj.removeReference(); // 引用 -1,若为0则可回收

当然,实际 JVM 中并未使用这种方法来管理对象生命周期。

优缺点分析

优势劣势
算法实现简单,效率高无法处理循环引用
回收实时性好增加引用维护成本
易于实现跨语言互操作与现代 JVM 架构不兼容

因此,虽然引用计数法在一些脚本语言(如 Python)或 C++ 的智能指针中应用较多,但在 Java JVM 中并未成为主流方法。

下一节我们将介绍 JVM 真正使用的对象存活判定方式:可达性分析法(Reachability Analysis)

4. 方法二:可达性分析法

GC Roots 概念

可达性分析法(Reachability Analysis)是目前 Java 虚拟机中对象存活判定的主流算法。

该方法的核心思想是:通过从一组称为 "GC Roots" 的起始节点出发,沿着对象引用链向下搜索,如果某个对象从 GC Roots 出发可达,则说明该对象是“活着”的;否则就会被判定为“死亡”。

GC Roots 的起始节点通常包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中 JNI 引用的对象

我们将在后续的章节中深入介绍这些 GC Roots 的类型。

分析流程

整个分析过程可以类比成遍历一张“对象图”:

  1. 建立对象引用图(Object Graph) 所有对象通过引用连接形成有向图,图中的边代表引用关系。

  2. 标记可达对象 从 GC Roots 出发,标记所有可到达的对象,形成“可达集合”。

  3. 未被标记的对象即为不可达对象 这些不可达的对象被视为垃圾,等待 GC 清理。

注意:即使对象不可达,JVM 并不会立刻回收它。 如果该对象覆盖了 finalize() 方法,还会进入一次“F-Queue”队列,被 GC 再次确认其是否真的不可用。

图解说明

            [GC Roots]|-------------------------|           |           |Obj1        Obj2       Obj3|                       |Obj4                   Obj5Obj6 (无法从 GC Roots 到达)
  • Obj1~Obj5 均从 GC Roots 可达,为存活对象。

  • Obj6 无任何引用链连接至 GC Roots,被视为“死亡对象”。

示例代码演示

虽然 JVM 自动完成对象图的构建和遍历,我们无法直接干预,但可以通过示例展示“对象是否可达”的效果:

public class ReachabilityDemo {static class Node {String name;Node reference;Node(String name) {this.name = name;}@Overrideprotected void finalize() throws Throwable {System.out.println(name + " 被回收了");}}public static void main(String[] args) {Node a = new Node("A");Node b = new Node("B");Node c = new Node("C");a.reference = b;b.reference = c;a = null; // 去除对 A 的强引用b = null; // 去除对 B 的强引用c = null; // 去除对 C 的强引用System.gc(); // 显式请求 GCtry {Thread.sleep(1000); // 等待 GC 完成} catch (InterruptedException e) {e.printStackTrace();}}
}

输出示例:

C 被回收了
B 被回收了
A 被回收了

说明 A、B、C 都在不可达状态下被 GC 回收。

优势与 JVM 的支持

优点说明
能解决循环引用问题不依赖引用计数值,识别结构关系
更适合复杂对象图图遍历可适配大型堆场景
JVM 官方支持Java 8 及以后的所有主流 JVM 均基于该方法

下一节我们将具体介绍 GC Roots 中的各类节点来源,帮助大家更深入理解对象“可达”的起点到底是什么。

 

5. GC Roots 的类型

在上一节中我们提到,GC Roots 是可达性分析的起点。那么,GC Roots 到底是什么?哪些对象或引用属于 GC Roots?理解 GC Roots 是掌握 JVM 垃圾回收机制的核心一步。

GC Roots 主要包括以下几种类型的引用:

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

每个线程在执行方法时都会创建一个栈帧(Stack Frame),其中的局部变量表中保存着各种基本类型和对象引用。

public class StackReferenceDemo {public static void main(String[] args) {Object obj = new Object(); // obj 是 GC Root 引用System.gc();}
}

在这个例子中,obj 是定义在主方法中的局部变量,它保存在栈帧的局部变量表中,因此是 GC Roots。

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

静态字段随着类的加载而存在于方法区中,引用的对象也会被视为 GC Roots。

public class StaticReferenceDemo {private static Object staticObj = new Object(); // 属于 GC Rootpublic static void main(String[] args) {System.gc();}
}

即使没有局部变量引用 staticObj,它依然不会被 GC,因为它是类的静态属性。

3. 方法区中常量引用的对象

常量池中的引用,如字符串常量等,也是 GC Roots 的一部分。

public class ConstantPoolDemo {public static void main(String[] args) {String str = "hello world"; // 字符串常量常驻内存System.gc();}
}

在这个例子中,字符串 "hello world" 常驻在运行时常量池中,是 GC Roots 的一部分,不会被回收。

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

如果 Java 程序调用了本地方法(如 C/C++ 实现的库),这些 native 方法中持有的对象引用也会被当作 GC Roots。

public class JNIDemo {static {System.loadLibrary("native-lib");}public native void callNative();
}

虽然无法用 Java 展示 native 层引用的具体内容,但这些引用 JVM 会在 GC 时特殊处理。

5. 活跃线程

所有运行中的线程(如主线程、GC线程、后台线程等)都是 GC Roots,因为它们自身的引用链天然“存活”。只有当线程执行结束、退出后,它们才会从 GC Roots 移除。

6. JVM 内部保留的系统类加载器

例如 sun.misc.Launcher$AppClassLoaderExtClassLoader 等,这些类加载器加载的类及其引用的对象会被视为 GC Roots。

7. JDK 特殊结构

System.in/out/err、线程上下文类加载器、反射中的 Method/Field/Constructor 对象、线程组等。这些结构大多存在于系统级类中,使用时容易导致内存泄露。

总结 GC Roots 类型

GC Roots 类型是否常见是否手动可控
虚拟机栈引用✅ 常见✅ 可控
静态属性引用✅ 常见✅ 可控
常量池引用✅ 常见❌ 不建议操作
JNI 本地引用❗ 复杂❌ 不建议操作
活跃线程引用✅ 常见❌ 不可控
类加载器引用✅ 常见❌ 不可控
系统类结构引用✅ 隐蔽❌ 不可控

理解 GC Roots 的种类不仅有助于判断哪些对象能被 GC 回收,也对分析内存泄露、优化引用管理非常有帮助。

在下一节中,我们将进一步探索 Java 中的 finalize() 机制,以及对象“抢救”自己的最后机会。

6. Finalize 机制与固定对象

即使一个对象在 GC Roots 的可达性分析中被判定为“不可达”,也不代表它立刻会被回收。Java 提供了一个“临终遗言”机制,即 finalize() 方法,使对象有一次自我拯救的机会。

6.1 什么是 finalize()

finalize()java.lang.Object 类中的一个方法:

protected void finalize() throws Throwable {// 释放资源或对象复活的钩子方法
}

当对象第一次被判定为不可达时,GC 会检查该对象是否覆盖了 finalize() 方法,且该方法是否尚未被调用。如果满足条件,JVM 会将该对象放入一个名为 Finalization Queue 的队列中,由一个低优先级的 Finalizer 线程去执行其 finalize() 方法。

注意:每个对象的 finalize() 方法最多只会被调用一次。

6.2 finalize() 能做什么?

  • 释放资源:用于释放文件句柄、关闭网络连接等非内存资源(但不推荐这么用,推荐使用 try-with-resources)。

  • 复活对象:对象在 finalize() 中如果再次赋值给 GC Roots 引用链中的某个变量,则对象会“复活”。

6.3 示例:对象的自我拯救

public class FinalizeRescueDemo {public static FinalizeRescueDemo OBJ = null;@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("finalize() 方法被调用");OBJ = this; // 对象复活!}public static void main(String[] args) throws InterruptedException {OBJ = new FinalizeRescueDemo();// 第一次 GC,对象有机会复活OBJ = null;System.gc();Thread.sleep(1000);System.out.println(OBJ != null ? "对象存活" : "对象死亡");// 第二次 GC,finalize() 不会再被调用OBJ = null;System.gc();Thread.sleep(1000);System.out.println(OBJ != null ? "对象存活" : "对象死亡");}
}

运行结果:

finalize() 方法被调用
对象存活
对象死亡

说明:第一次 GC 时 finalize() 被调用,OBJ 被重新引用,从而复活。第二次 GC 时不再执行 finalize(),对象被真正回收。

6.4 finalize() 的问题与风险

  • 不可控时机:执行时间不确定,依赖 GC。

  • 影响性能:JVM 要维护一个队列和额外线程。

  • 风险隐患:对象复活逻辑可能导致资源泄露或更难以调试的 bug。

  • 已被废弃:Java 9 开始标注为 @Deprecated,建议使用 java.lang.ref.Cleaner 替代。

6.5 替代方案:Cleaner

import java.lang.ref.Cleaner;public class CleanerDemo {private static final Cleaner cleaner = Cleaner.create();static class Resource implements Runnable {@Overridepublic void run() {System.out.println("资源被清理");}}public static void main(String[] args) {Object obj = new Object();cleaner.register(obj, new Resource());}
}

Cleaner 提供了比 finalize() 更轻量、可控的资源清理方式,推荐在现代 Java 项目中使用。

7. 不同引用类型与垃圾回收行为

Java Reference类及其实现类深度解析:原理、源码与性能优化实践

Java 中定义了四种不同级别的引用类型:强引用(Strong Reference)软引用(Soft Reference)弱引用(Weak Reference)虚引用(Phantom Reference),它们在 JVM 中表现出不同的“生存权重”。理解这些引用类型对于资源缓存、内存优化和对象生命周期控制至关重要。

7.1 强引用(Strong Reference)

这是最常见的引用类型:

Object obj = new Object();

只要强引用还存在,GC 永远不会回收该对象。

特性

  • 是默认引用类型。

  • 会阻止 GC 回收所指向的对象。

示例

public class StrongReferenceDemo {public static void main(String[] args) {Object obj = new Object();System.gc();System.out.println(obj != null ? "对象未被回收" : "对象被回收");}
}

输出:对象未被回收

7.2 软引用(Soft Reference)

软引用是一种比较“温柔”的引用。它在内存不足时才会被 GC 回收。

SoftReference<Object> softRef = new SoftReference<>(new Object());

常用于内存敏感的缓存。

示例

import java.lang.ref.SoftReference;public class SoftReferenceDemo {public static void main(String[] args) {Object obj = new Object();SoftReference<Object> softRef = new SoftReference<>(obj);obj = null;System.gc();if (softRef.get() != null) {System.out.println("软引用对象仍存活");} else {System.out.println("软引用对象被回收");}}
}

注意:此示例中的回收依赖内存状况,可能不会立即触发。

7.3 弱引用(Weak Reference)

弱引用在 GC 时总是会被回收。

WeakReference<Object> weakRef = new WeakReference<>(new Object());

特性

  • 非常适合使用在 ThreadLocal、元数据缓存等短生命周期场景。

示例

import java.lang.ref.WeakReference;public class WeakReferenceDemo {public static void main(String[] args) {Object obj = new Object();WeakReference<Object> weakRef = new WeakReference<>(obj);obj = null;System.gc();if (weakRef.get() != null) {System.out.println("弱引用对象仍存活");} else {System.out.println("弱引用对象被回收");}}
}

输出:弱引用对象被回收

7.4 虚引用(Phantom Reference)

虚引用无法通过 get() 方法访问,被用于对象被回收时收到通知。

PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue);

特点

  • 永远不会阻止 GC。

  • 常与 ReferenceQueue 配合使用。

示例

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;public class PhantomReferenceDemo {public static void main(String[] args) {Object obj = new Object();ReferenceQueue<Object> queue = new ReferenceQueue<>();PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);obj = null;System.gc();System.out.println("phantomRef.get(): " + phantomRef.get());System.out.println("是否进入 ReferenceQueue: " + (queue.poll() != null));}
}

输出:

phantomRef.get(): null
是否进入 ReferenceQueue: true

说明:虚引用不会返回实际对象,只用于跟踪对象是否已被 GC。

7.5 引用强度对比总结

引用类型是否影响 GC 回收典型用途是否可通过 get() 访问对象
强引用普通对象引用
软引用视内存情况而定内存敏感缓存
弱引用ThreadLocal、临时元数据
虚引用是(立即回收)清理前回调通知

 

8. 垃圾收集器与对象存活判定策略

Java 虚拟机中的垃圾收集器(GC)负责自动管理堆内存,及时回收不再使用的对象。不同的垃圾收集器采用不同的算法和策略来判断对象是否存活,从而决定是否回收。理解这些策略有助于优化程序性能和内存管理。

8.1 常见垃圾收集器简介

收集器名称特点适用场景
Serial GC(串行收集器)单线程执行,简单高效适合小内存或单核环境
Parallel GC(并行收集器)多线程并行,吞吐量优先多核服务器环境
CMS GC(并发标记清理)低停顿,标记与清理并发执行对响应时间敏感的应用
G1 GC(Garbage First)分区管理,低停顿,适合大堆大内存多核服务器

 

8.2 对象存活判定的核心机制

无论使用哪种收集器,对象的存活判定都基于“可达性分析”(Reachability Analysis):

  • 从 GC Roots(如线程栈、静态变量)开始,遍历所有引用链。

  • 能被引用链访问到的对象被认为是存活的,不回收。

  • 无法访问的对象则被标记为可回收。

8.3 不同收集器的对象判定流程

Serial 和 Parallel 收集器

  • 标记-清除(Mark-Sweep)或标记-复制(Mark-Copy)算法

  • 先暂停应用(Stop-The-World),从 GC Roots 开始标记存活对象。

  • 清除未标记对象或复制存活对象到新空间。

CMS 收集器

  • 采用多阶段并发标记:

    • 初始标记:暂停应用,标记直接可达对象。

    • 并发标记:应用线程运行时,标记间接可达对象。

    • 重新标记:短暂停止应用,完成标记遗漏部分。

    • 并发清理:清理不可达对象。

G1 收集器

  • 将堆划分成多个固定大小的区域(Region)。

  • 并发标记阶段识别每个区域的存活对象数量。

  • 优先回收存活对象少的 Region,减少停顿时间。

  • 支持混合回收:回收年轻代和部分老年代。

8.4 代码示例:指定收集器启动参数

# 使用 Serial GC
java -XX:+UseSerialGC -Xmx512m -Xms512m MyApp# 使用 CMS GC
java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g MyApp# 使用 G1 GC
java -XX:+UseG1GC -Xmx4g -Xms4g MyApp

使用 VisualVM 或 JVisualVM 可以观察不同收集器下堆内存对象的存活情况。

9. 总结与实践建议

本文全面解析了 JVM 中对象存活判定的核心机制及其应用,包括可达性分析、引用类型、Finalize机制、垃圾收集器对判定策略的影响等关键内容。

9.1 对象存活判定的核心是“可达性分析”

  • 通过从 GC Roots 出发遍历引用链,判断对象是否仍被程序访问。

  • 只有不可达对象才有回收资格,确保安全且高效的内存管理。

9.2 多种引用类型助力内存优化

  • 强引用、软引用、弱引用、虚引用各具特点,开发者可根据需求选择不同引用,灵活控制对象生命周期和内存回收时机。

  • 理解它们的差异,有助于避免内存泄漏和提升程序稳定性。

9.3 Finalize机制存在风险,应尽量避免

  • finalize() 方法虽可让对象“复活”,但执行时机不确定,且影响性能。

  • 推荐使用 java.lang.ref.Cleaner 替代,更加安全且高效。

9.4 不同垃圾收集器对对象存活判定实现有差异

  • 串行、并行、CMS 和 G1 GC 等采用各自的标记算法和阶段,平衡吞吐量与延迟。

  • 了解垃圾收集器特性,合理配置 GC 参数,对提升系统性能至关重要。

9.5 实践建议

  • 在开发中,优先确保对象引用链清晰,避免意外的强引用导致内存泄漏。

  • 结合软弱引用,设计缓存等场景,提高内存利用率。

  • 监控和调优垃圾收集器,配合性能分析工具,及时发现和解决内存相关问题。

  • 避免依赖 finalize(),转用 Cleaner 和显式资源管理。

  • 对于大型应用,考虑采用 G1 或者更先进的收集器,兼顾响应和吞吐。

通过深入理解对象存活判定方法,开发者能更精准地控制内存管理,写出高效、稳定的 Java 应用。

 

http://www.lryc.cn/news/586077.html

相关文章:

  • 同步、异步、阻塞、非阻塞之间联系与区别
  • Windows符号链接解决vscode和pycharm占用C盘空间太大的问题
  • [论文阅读] 人工智能 + 软件工程 | AI助力软件可解释性:从用户评论到自动生成需求与解释
  • 利用scale实现分页按钮,鼠标经过按钮放大
  • 12.使用VGG网络进行Fashion-Mnist分类
  • 解决bash终端的路径名称乱码问题
  • java单例设计模式
  • pip国内镜像源一览
  • 高校/企业/医院食堂供应链平台开发详解:采购系统源码的核心价值
  • MySQL 中图标字符存储问题探究:使用外挂法,毕业论文——仙盟创梦IDE
  • Oxygen XML Editor 26.0编辑器
  • 车载诊断架构 --- 诊断功能开发流程
  • Operation Blackout 2025: Smoke Mirrors
  • 日志不再孤立!用 Jaeger + TraceId 实现链路级定位
  • 传感器WSNs TheDataLinkLayer——X-MAC
  • 前端开发中的输出问题
  • try-catch-finally可能输出的答案?
  • [BUUCTF 2018]Online Tool
  • MCP上的数据安全策略:IAM权限管理与数据加密实战
  • Vim的magic模式
  • QT跨平台应用程序开发框架(5)—— 常用按钮控件
  • RS232通信如何实现(硬件部分)
  • 请求服务端获取broker的机房归属信息异常
  • 端到端自动驾驶:挑战与前沿
  • Unity URP + XR 自定义 Skybox 在真机变黑问题全解析与解决方案(支持 Pico、Quest 等一体机)
  • 时序数据预处理
  • Javaweb总结一
  • AV1高层语法
  • 【Elasticsearch 】search_throttled
  • (LeetCode 面试经典 150 题 ) 209. 长度最小的子数组(双指针)