首页 > Java > java教程 > 正文

如何判断一个对象是否可以被回收?(引用计数法、可达性分析法)

紅蓮之龍
发布: 2025-09-05 16:33:01
原创
188人浏览过
判断一个对象是否可回收,核心在于其能否被程序的活跃部分引用。若对象无法从GC Roots触达且无强引用,则被视为垃圾。主要依赖引用计数法和可达性分析法。引用计数法因循环引用问题易导致内存泄漏,如A引用B且B引用A时,计数永不归零,对象无法回收。现代JVM多采用可达性分析法,从GC Roots(如栈变量、静态属性、常量、JNI引用、活跃线程)出发遍历对象图,不可达对象被回收。为避免STW,现代GC采用并发标记,结合增量更新或SATB策略处理并发修改,辅以读屏障等技术,实现低延迟回收。

如何判断一个对象是否可以被回收?(引用计数法、可达性分析法)

一个对象是否可以被回收,核心判断依据在于它是否仍然被程序中的“活跃”部分所引用。简单来说,如果一个对象不再有任何强引用指向它,并且从程序的根节点(GC Roots)开始遍历时无法触达,那么它就可以被垃圾回收器视为“垃圾”,等待被清理。这主要依赖于两种策略:引用计数法和可达性分析法。

解决方案

在我看来,判断一个对象是否该“寿终正寝”了,这背后其实是内存管理机制的一场博弈。我们程序运行时产生的各种数据,总得有个地方放,也总得有个机制来决定什么时候能把它们腾出来,给新的数据用。引用计数法和可达性分析法,就是这场博弈中两种截然不同的裁判规则。

引用计数法是一种相对直观的策略。它的基本思想是,给每个对象维护一个引用计数器。每当有一个地方引用它,计数器就加一;当引用失效时,计数器就减一。一旦计数器归零,这个对象就意味着没有其他对象再“关心”它了,自然就可以被回收了。这种方式的好处是,回收时机非常明确,几乎是实时的,一旦引用归零,对象就可以被立即回收,省去了等待垃圾回收器统一调度的时间。比如,像Python这样的语言,就部分采用了引用计数,但它也清楚地认识到这种方法的局限性,所以还辅以其他机制来弥补。

然而,仅仅依赖引用计数,在实际的复杂场景中是远远不够的。它的最大痛点在于无法解决“循环引用”的问题。想象一下,对象A引用了对象B,同时对象B又引用了对象A。如果除此之外,没有其他任何外部引用指向A或B,那么它们的引用计数永远不会降为零。即便它们已经对程序没有任何用处了,它们也会像一对互相依偎的僵尸,永远霸占着内存空间,造成内存泄漏。这就是为什么现代主流的虚拟机,比如Java的JVM,更倾向于采用另一种更强大的判断方式——可达性分析法

可达性分析法,听起来可能有点抽象,但它的逻辑其实很像我们平时找东西。它会从一系列被称为“GC Roots”的根对象开始,沿着这些根对象所引用的路径,一步步地去遍历所有能够被触达到的对象。所有在遍历过程中能够被“标记”到的对象,都说明它们仍然是“活的”,程序还在使用它们。而那些从任何GC Roots出发都无法到达的对象,就自然而然地被判定为是不可用的,也就是可以被回收的垃圾。这种方法巧妙地避开了循环引用的难题,因为即使A和B互相引用,只要它们都无法从GC Roots被触达,它们最终都会被回收。这就像一张巨大的蜘蛛网,GC Roots就是网的固定点,所有能顺着网线爬到的地方都是安全的,爬不到的地方就等着被清理掉。

为什么引用计数法无法彻底解决内存泄漏问题?

引用计数法之所以无法彻底解决内存泄漏,其根本原因就在于它在处理循环引用时的无力。我们可以这样设想一个场景:你创建了两个对象,

objectA
登录后复制
objectB
登录后复制
objectA
登录后复制
内部有一个字段指向
objectB
登录后复制
,而
objectB
登录后复制
内部也有一个字段指向
objectA
登录后复制
。现在,假设除了这两个对象彼此之间的引用之外,程序中再也没有其他任何地方引用
objectA
登录后复制
objectB
登录后复制
了。

从我们人类的逻辑来看,这两个对象已经没有任何实际用处了,它们应该被回收。但是,如果使用引用计数法,

objectA
登录后复制
的引用计数会因为
objectB
登录后复制
的引用而保持为1,
objectB
登录后复制
的引用计数也会因为
objectA
登录后复制
的引用而保持为1。它们各自的计数器永远不会归零,因此垃圾回收器就永远不会认为它们是可回收的对象,即便它们已经“死”了。它们会一直占据着内存,直到程序结束,这就是典型的内存泄漏。

这种问题在复杂的软件系统中非常常见,尤其是在对象之间存在复杂依赖关系时。像Python这样的语言,虽然主要使用引用计数,但为了解决这个问题,它会额外引入一个周期检测器(cycle detector)来专门查找并打破循环引用,这无疑增加了系统的复杂性。所以,在我看来,引用计数法虽然直观且易于实现,但在面对现代软件的复杂对象图时,它的局限性就暴露无遗了。

法语写作助手
法语写作助手

法语助手旗下的AI智能写作平台,支持语法、拼写自动纠错,一键改写、润色你的法语作文。

法语写作助手 31
查看详情 法语写作助手

现代JVM中,哪些对象会被视为GC Roots?

在Java的JVM中,GC Roots是可达性分析算法的起点,它们是那些被认为“不可回收”的、能够被程序直接访问到的引用。理解GC Roots非常关键,因为它决定了哪些对象是“活”的。通常,以下几类对象会被JVM视为GC Roots:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象: 当一个方法被调用时,JVM会为它创建一个栈帧,其中包含本地变量表。本地变量表里存放着各种基本类型数据和对象引用。如果一个对象被本地变量表中的引用所指向,那么这个对象就是GC Root。举个例子,你在一个方法里声明了一个
    Object obj = new Object();
    登录后复制
    ,那么
    obj
    登录后复制
    这个引用就指向了栈上的一个对象,这个对象就是GC Root。
  • 方法区中类静态属性引用的对象: 在Java中,类的静态变量(
    static
    登录后复制
    字段)是存储在方法区(在JDK 8及以后,通常是元空间的一部分)的。如果一个静态变量引用了一个对象,那么这个对象也会被视为GC Root。这是因为静态变量的生命周期与类本身相同,只要类还存在,这个静态引用就一直存在。
  • 方法区中常量引用的对象: 比如字符串常量池中的对象,或者通过
    final
    登录后复制
    关键字修饰的常量,它们被方法区中的常量表引用。这些对象也是GC Roots。
  • 本地方法栈中JNI(Native方法)引用的对象: 当Java程序调用本地方法(Native Method,例如C或C++代码)时,本地方法栈会为这些方法服务。在本地方法中,可能会创建或引用Java对象。这些被本地方法引用的对象,也必须被视为GC Roots,以防止在本地代码还在使用它们时被垃圾回收。
  • 所有活跃线程本身: 线程对象本身也是GC Roots。因为线程是程序执行的最小单位,它的存在意味着程序还在运行,它所持有的引用自然是“活”的。

这些GC Roots就像是内存中的“锚点”,它们是垃圾回收器进行可达性分析的起点。所有能够通过这些锚点被直接或间接访问到的对象,都将被标记为“存活”对象,不会被回收。反之,那些无法从任何GC Root追溯到的对象,才会被判定为可回收。

可达性分析法在实际应用中是如何避免“暂停一切”(Stop-The-World)的?

“Stop-The-World”(STW)是垃圾回收领域一个让人头疼的词。在早期的可达性分析实现中,为了保证对象图的准确性,垃圾回收器在执行标记阶段时,需要暂停所有用户线程(mutator),直到标记工作完成。这会导致应用程序出现明显的卡顿,对于追求低延迟和高并发的应用来说是难以接受的。为了解决这个问题,现代的JVM垃圾回收器引入了许多高级技术来减少或避免STW,主要思路是让标记过程与用户线程并发执行。

其中最核心的技术之一是并发标记(Concurrent Marking)。它允许垃圾回收器在用户线程运行的同时进行对象的标记。但并发标记会带来一个新的挑战:在GC标记过程中,用户线程可能会修改对象引用关系,导致已经标记的对象变得不可达,或者未标记的对象变得可达。这被称为“漏标”和“错标”问题。

为了解决这些问题,现代垃圾回收器通常会采用以下两种策略:

  1. 增量更新(Incremental Update):这种策略认为,只要一个对象在标记阶段被用户线程修改了引用关系,使得它从一个已标记对象指向了一个未标记对象(即“新”引用指向“旧”未标记对象),那么这个“旧”的未标记对象就应该被重新标记。它的核心思想是,当一个引用从已标记对象指向未标记对象时,就把这个引用记录下来,在下一次GC周期或者特定的阶段重新扫描这些记录的引用。这就像给那些可能被“遗漏”的对象打上一个补丁,确保它们不会被错误回收。
  2. 原始快照(Snapshot-At-The-Beginning, SATB):SATB策略则更激进一些。它假设在GC开始标记的那一刻,所有存活的对象都已经被“拍了一张快照”。如果在GC标记过程中,用户线程删除了一个从已标记对象到未标记对象的引用(即“旧”引用被删除),那么这个被删除的引用所指向的对象,即使在快照后变得不可达,也仍然会被认为是存活的。换句话说,它只关心对象在GC开始时的可达性,对于后续的引用删除操作不敏感。这可以避免漏标问题,但可能会导致一些“浮动垃圾”(即本可以回收但因为SATB而保留到下一轮GC的对象)。

例如,G1垃圾回收器就采用了SATB来处理并发标记阶段的并发问题。ZGC和Shenandoah等更先进的垃圾回收器则在此基础上,结合了读屏障(Read Barrier)等技术,进一步减少甚至几乎消除了STW时间,它们通过在对象被读取时进行额外的操作来维护对象图的准确性,使得GC暂停时间可以控制在非常低的毫秒级别,甚至亚毫秒级别,从而实现对应用程序几乎无感知的垃圾回收。这些技术的引入,使得JVM在保证内存安全的同时,也能满足现代应用对高性能和低延迟的严苛要求。

以上就是如何判断一个对象是否可以被回收?(引用计数法、可达性分析法)的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号