Java - 垃圾回收器如何快速知道哪些对象不再被引用?

25

我了解在Java中,如果一个对象没有任何引用与之关联,垃圾收集器会在以后的某个时刻将其回收。

但是垃圾收集器如何知道一个对象是否有引用与之关联?

垃圾收集器是否使用某种哈希表或表格?


编辑:

请注意,我不是在询问垃圾回收机制怎么工作。实际上,我并不是在问那个。

我的问题是具体地,垃圾收集器如何高效地知道哪些对象是活动的,哪些是无用的。

这就是为什么我在问题中说,垃圾回收是否维护一种哈希表或集合,并定期更新一个对象所引用的数量。


1
可能是Java垃圾回收背后的理论和算法的重复问题。 - Ernest Friedman-Hill
@ErnestFriedman-Hill 不,这个问题不是重复的https://dev59.com/ZW855IYBdhLWcg3wy3ob。我不是在问垃圾收集的基本理论。相反,我具体地询问垃圾收集器如何管理对象当前拥有的引用数量,以便稍后收集器可以轻松决定是否回收它。 - Jackson Tale
这正是“垃圾回收的基本理论”。 - Ernest Friedman-Hill
@ErnestFriedman-Hill,关键是那篇文章并没有回答我的问题。 - Jackson Tale
5个回答

13
一种典型的现代 JVM 使用多种不同类型的垃圾收集器。其中经常用于存活一段时间的对象的一种称为标记-清除。它基本上涉及从已知的“活动”对象(所谓的垃圾收集根)开始,跟随所有对象引用链,并将每个可访问的对象标记为“活动”。完成后,清除阶段可以回收那些未被标记为“活动”的对象。为使该过程正常工作,JVM 必须知道每个对象引用在内存中的位置。这是垃圾收集器精确(Java 中的垃圾收集器就是)所必需的条件。

那么你的意思是每次垃圾回收器进行清理时,它都会扫描所有内存中的对象(遵循标记和清除过程)吗?这样有效吗? - Jackson Tale
我的意思是,如果我在内存中有数百万个对象,每次进行完整扫描可能不太有效。 - Jackson Tale
@JacksonTale:了解某件事是否存在问题的唯一方法是对其进行测量。这就是分析器的作用(它们可以告诉您有关GC正在发生什么的很多信息)。 - NPE

6

Java有各种不同的垃圾收集策略,但它们基本上都通过跟踪哪些对象是从已知活动对象可达的来工作。

在文章《Java中垃圾回收的工作原理》中可以找到一个很好的总结,但如果您想了解更详细的信息,您应该查看《使用5.0 Java[tm]虚拟机调整垃圾收集》

当一个对象在运行程序中的任何指针都无法访问时,它被认为是垃圾。最直接的垃圾回收算法只是遍历每个可达对象。剩下的对象被视为垃圾。这种方法所需的时间与活动对象的数量成正比,对于维护大量实时数据的大型应用程序来说是不可行的。
从J2SE平台1.2版本开始,虚拟机结合使用多种不同的垃圾回收算法,使用分代收集。尽管朴素的垃圾回收检查堆中的每个活动对象,但分代收集利用了大多数应用程序的几个经验观察到的属性,以避免额外的工作。
其中最重要的观察到的属性是婴儿期死亡率...即许多对象(如迭代器)只存在很短的时间,因此“年轻”的对象比较老的对象更有可能被垃圾回收。
有关更多最新的调整指南,请参阅:

顺便提一下,要小心尝试猜测垃圾回收策略,我知道有许多程序因为过度使用System.gc()或不适当的-XX选项而性能受到破坏。


2
事实上,垃圾回收器通常无法快速知道哪些对象不再具有任何传入引用。实际上,即使存在传入引用,某个对象也可能是垃圾。
垃圾回收器使用对象图的遍历来查找可达对象。在此遍历中未到达的对象被视为垃圾,即使它们是引用循环的一部分。对象变得不可达和垃圾回收器实际收集对象之间的延迟可以是任意长的。

2

GC会尽可能快地知道哪些对象可以被移除。您不需要管理此过程。

但您可以非常礼貌地使用System.gc()请求GC运行。这只是向系统发出的一个提示。GC此时不必运行,也不必删除您特定的对象等。因为GC是“老大”,而我们(Java程序员)只是它的奴隶... :(


2
+1 给 GC 统治。垃圾 Chucknorris 只接收问题,但从不接受命令。这就像请求服务并在服务器决定后得到答案。 - Fritz

0

没有一种 高效的 方式 - 它仍然需要遍历堆,但是 有一种诡异的方式:当将堆分成较小的部分时(因此无需扫描整个堆),这就是我们拥有代际垃圾收集器的原因,使得扫描花费的时间更少。

当你的整个应用程序停止并且可以分析对象图形时,这相对“容易”回答。它始于 GC roots (我会让您找到这些是什么的文档),但基本上这些是由GC 不收集的“根”。

从这里开始进行某种扫描,它分析"活"对象:与这些根有直接(或传递)连接的对象,因此不能回收。在图形理论中,通过使用3种颜色:黑色、灰色和白色来着色/遍历图形。 白色表示它与根没有连接,灰色表示它的子图尚未遍历,黑色表示已经遍历并与根相连。所以要知道现在究竟是什么死/活 - 你只需要把所有最初为白色的堆都染成黑色。一切 白色 都是垃圾。有趣的是,通过了解实际存活的内容, GC 确认了 "垃圾"。这里有一些图形可视化 here 举例。

但这是简单的情况:当您的应用程序完全停止(有时为几秒钟),您可以扫描堆。这称为STW - 停止全球事件,通常人们不喜欢这种情况。这就是并行收集器所做的:停止一切,进行所有GC操作(包括查找垃圾),然后让应用程序线程开始运行。

当您的应用程序正在运行并且您正在扫描堆时会发生什么?Concurrently? G1/CMS就是这样做的。请考虑一下:当您的应用程序可以通过另一个线程更改该叶子时,您如何推断出图形中的叶子是否存在。

Shenandoah 例如通过“拦截”图形上的更改来解决此问题。在与您的应用程序并发运行时,它将捕获所有更改并将这些更改插入到一些线程本地特殊队列中,称为SATB Queues(开始时快照队列);而不是直接更改堆。完成后,将发生非常短的STW事件,并且将清空这些队列。在STW下,计算由该排水“引起”的内容,即:图形的额外着色。这远远简化了,仅供参考。G1CMS使用不同的方法,据我所知。


理论上,这个过程并不是很复杂,但同时实现它是最具挑战性的部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接