Java垃圾回收如何处理循环引用?

180

据我所知,Java中的垃圾回收会清理一些对象,如果没有其他东西“指向”该对象。

我的问题是:如果我们有以下这种情况会发生什么:

class Node {
    public object value;
    public Node next;
    public Node(object o, Node n) { value = 0; next = n;}
}

//...some code
{
    Node a = new Node("a", null), 
         b = new Node("b", a), 
         c = new Node("c", b);
    a.next = c;
} //end of scope
//...other code

abc应该被垃圾回收,但它们都被其他对象引用。

Java的垃圾回收机制如何处理这种情况?(或者只是内存泄漏吗?)


1
请参见:https://dev59.com/B3RC5IYBdhLWcg3wG9Rb,特别是来自@gnud的第二个答案。 - Seth
9个回答

180

Java的GC认为如果对象不通过从垃圾回收根开始的链路可达,则它们是“垃圾”,因此这些对象将被回收。即使对象相互引用形成循环,如果与根断开联系,它们仍然是垃圾。

请参阅附录A中关于不可访问对象的部分:《Java平台性能:策略与实战》了解详细信息。


20
你有相关的参考资料吗?这个很难测试。 - tangens
7
我已添加一个引用。你还可以覆盖一个对象的finalize()方法来确定它何时被回收(尽管这是我唯一推荐使用finalize()的事情)。 - Bill the Lizard
4
“...smart enough to recognize...” 这句话听起来有些困惑。GC 不需要识别循环引用,因为这些对象已经无法访问,所以它们是垃圾。 - Alexander Malakhov
1
@Alexander:你说得对。我最初的措辞让人感觉像是垃圾回收器在分析内存中的所有对象并显式标记循环引用。实际情况并不那么复杂,所以我重新表述了我的答案。 - Bill the Lizard
99
在一次有关垃圾回收的讨论中,@tangens问道:“你有相关的参考资料吗?”最好的双关语。 - Michał Kosmulski
显示剩余5条评论

157

是的,Java垃圾收集器可以处理循环引用!

How?

有一种特殊的对象叫做垃圾回收根(GC roots)。它们总是可达的,如果一个对象有它们作为自己的根,那么该对象也是可达的。

简单的 Java 应用程序具有以下 GC 根:

  1. 主方法中的局部变量
  2. 主线程
  3. 主类的静态变量

enter image description here

为了确定哪些对象不再使用,JVM 会间歇性地运行一个非常恰当的算法,称为标记-清除算法。其工作如下:

  1. 算法遍历所有对象引用,从 GC 根开始,并将找到的每个对象标记为存活。
  2. 所有未被标记对象所占用的堆内存都会被回收。这些对象没有被使用,所以只需将它们标记为空闲即可,实际上就像扫除未使用的对象一样。

因此,如果任何对象不可从 GC 根访问(即使它是自引用或循环引用),它都将被垃圾回收。

当然,如果程序员忘记取消引用某个对象,有时会导致内存泄漏。

enter image description here

来源:Java 内存管理


3
完美的解释!谢谢! :) - Jovan Perovic
感谢您分享那本书。它充满了关于Java开发和其他相关主题的重要信息! - Droj
19
最后一张图片中有一个无法到达的物体,但它被放在了可到达物体的部分。 - La VloZ Merrill
在最后一个图表的“可达区域”中仍有三个无法到达的对象。 - Zarremgregarrok

17
您是正确的。您所描述的垃圾回收特定形式被称为“引用计数”。最简单情况下,它的工作方式如下(至少在概念上是这样的,现代大多数引用计数实现实际上是以不同的方式实现的):
  • 每当添加一个对对象的引用时(例如将其赋给变量或字段、传递给方法等),它的引用计数增加1。
  • 每当移除一个对对象的引用时(方法返回、变量超出范围、字段重新分配给不同的对象或包含该字段的对象本身被垃圾回收),引用计数减1。
  • 一旦引用计数达到0,就没有更多的引用指向该对象,这意味着没有人可以再使用它,因此它是垃圾并且可以被回收。
而这种简单的策略正如您所描述的存在问题:如果A引用B,B引用A,则它们的引用计数永远不会小于1,这意味着它们永远不会被回收。
有四种方法解决这个问题:
  1. 忽略它。如果您有足够的内存,您的循环很小且不频繁,并且您的运行时间很短,也许您可以不收集循环。想象一下一个shell脚本解释器:shell脚本通常只运行几秒钟,并且不会分配太多内存。
  2. 将引用计数垃圾回收器与另一个没有循环问题的垃圾回收器结合使用。例如,CPython就是这样做的:CPython中的主要垃圾回收器是引用计数收集器,但不时会运行跟踪垃圾回收器来收集循环。
  3. 检测循环。不幸的是,在图形中检测循环是一项相当昂贵的操作。特别是,它需要几乎与跟踪收集器相同的开销,因此您可以直接使用其中之一。
  4. 不要以你我都会的天真方式实现算法:自70年代以来,已经开发了多个相当有趣的算法,它们以巧妙的方式将循环检测和引用计数结合为单个操作,这比单独执行它们或执行跟踪收集器要便宜得多。

顺便提一下,实现垃圾收集器的另一种主要方法(我已经在上面暗示过几次)是跟踪。跟踪收集器基于可达性的概念。您从某些您知道始终是可达的根集开始(例如全局常量,或Object类,当前词法作用域,当前堆栈帧),然后从那里跟踪从根集可达的所有对象,然后从可从根集中可达的对象跟踪所有可达的对象等等,直到您拥有传递闭包。不在该闭包中的任何内容都是垃圾。

由于循环仅在其内部可达,但无法从根集中可达,因此将被清除。


2
由于问题是针对Java的,我认为值得一提的是Java不使用引用计数,因此问题不存在。此外,维基百科链接作为“进一步阅读”将会很有帮助。总体而言,这是一个很好的概述! - Alexander Malakhov
我刚刚读了你对Jerry Coffin帖子的评论,所以现在我不太确定 :) - Alexander Malakhov

14

垃圾收集器从一些始终被认为是“可达”的位置开始,例如CPU寄存器、堆栈和全局变量。它通过查找这些区域中的任何指针,并递归地查找它们所指向的所有内容来工作。一旦发现了所有内容,其他所有内容都是垃圾。

当然,大多数情况下会有一些变化,主要是为了提高速度。例如,大多数现代垃圾收集器都是“分代”的,这意味着它将对象划分为代,随着对象变老,垃圾收集器在尝试确定该对象是否仍然有效或不再有效时,会越来越长时间地进行操作,即它只会假设如果对象已经存在很长时间,那么它还会继续存在更长时间的可能性非常大。

尽管如此,基本思想仍然是相同的:它基于从一些根集合开始的东西,这些东西它默认仍然可以使用,然后追踪所有指针以查找可能正在使用的其他内容。

有趣的是,许多人通常会对垃圾收集器的这部分和用于诸如远程过程调用等事物的对象编组代码之间的相似度感到惊讶。在每种情况下,您都是从一些根对象集合开始,并追踪指针以查找它们所引用的所有其他对象...


你所描述的是追踪收集器。还有其他类型的收集器。对于本讨论而言,特别感兴趣的是引用计数收集器,它们确实容易出现循环引用问题。 - Jörg W Mittag
@Jörg W Mittag:当然是真的——虽然我不知道有哪个(相当现代的)JVM使用引用计数,所以对于原始问题来说,它似乎不太可能产生太大的影响(至少对我来说是这样)。 - Jerry Coffin
至少默认情况下,我相信Jikes RVM目前使用的是Immix收集器,它是一种基于区域的追踪收集器(虽然它也使用引用计数)。我不确定您是否指的是那个引用计数,还是另一个使用引用计数而没有追踪的收集器(我猜后者,因为我从未听说过Immix被称为“回收器”)。 - Jerry Coffin
我有点混淆了:Recycler(已经?)在Jalapeno中实现,我想的算法是(已经?)在Jikes中实现的Ulterior Reference Counting。当然,说Jikes使用这个或那个垃圾收集器是相当无用的,因为Jikes和特别是MMtk是专门设计用于在同一JVM中快速开发和测试不同的垃圾收集器。 - Jörg W Mittag
2
Ulterior Reference Counting是由同一组人在2003年设计的,他们在2007年设计了Immix,因此我认为后者可能取代了前者。URC专门设计成可以与其他策略结合使用,事实上,URC论文明确提到URC只是通向结合追踪和引用计数优点的收集器的一个阶段。我想Immix就是那个收集器。无论如何,Recycler是一个引用计数收集器,尽管如此,它仍然可以检测和收集循环:http://WWW.Research.IBM.Com/people/d/dfb/recycler.html - Jörg W Mittag

9
Java的垃圾回收机制并不像你所描述的那样。更准确地说,它们从一个基础对象集合开始,通常称为“GC roots”,并将收集任何无法从根对象访问到的对象。
GC根包括以下内容:
  • 静态变量
  • 当前在运行线程的堆栈中的局部变量(包括所有适用的“this”引用)
因此,在您的情况下,一旦方法结束时局部变量a,b和c超出范围,就没有更多包含直接或间接引用到您的三个节点的GC根,它们就有资格进行垃圾回收。
如果您需要更多详细信息,请参阅TofuBeer的链接。

“...当前在一个正在运行的线程的堆栈中…” 它不是在扫描所有线程的堆栈以避免破坏其他线程的数据吗? - Alexander Malakhov

7

这篇文章(已不再提供)深入探讨了垃圾收集器的概念(在概念上有几种实现)。与您的帖子相关的部分是“ A.3.4 Unreachable”:

A.3.4 不可达 当没有更多强引用指向对象时,对象进入不可达状态。当一个对象不可达时,它就成为了一个收集的候选对象。请注意措辞:仅仅因为一个对象是收集的候选对象,并不意味着它会被立即收集。JVM可以自由地延迟收集,直到内存被对象占用而出现紧急需要。


1
点击这个直链跳转到该部分。 - Alexander Malakhov
1
链接已不再可用。 - titus

2
垃圾回收通常不意味着“仅在没有其他对象'指向'该对象时清除某个对象”(这是引用计数)。垃圾回收大致意味着找到程序无法访问的对象。
因此,在您的示例中,当a、b和c超出范围后,它们可以被GC收集,因为您无法再访问这些对象。

“垃圾回收大致意味着找到程序无法访问的对象。” 在大多数GC算法中,实际上正好相反。您从GC根开始查找可达对象,其余被视为未引用的垃圾。 - Fredrik
1
引用计数是垃圾回收的两种主要实现策略之一(另一种是跟踪)。 - Jörg W Mittag
3
大多数时候,当人们谈论垃圾收集器时,他们指的是基于某种标记和清除算法的收集器。如果没有垃圾收集器,通常会采用引用计数。虽然引用计数从某种意义上说是一种垃圾收集策略,但是几乎没有现有的垃圾收集器是基于它构建的,因此说它是一种垃圾收集策略只会让人们感到困惑,因为在实践中,它已经不再是一种垃圾收集策略,而是一种管理内存的替代方法。 - Fredrik

1

Bill直接回答了你的问题。正如Amnon所说,你对垃圾收集的定义只是引用计数。我只想补充一下,即使是非常简单的算法,如标记和清除以及复制收集,也可以轻松处理循环引用。所以,这并没有什么神奇的地方!


0

[JVM内存模型图]

GC从根(种子)开始标记,使用is used标志,这些根是stackpermanent。所有其他引用都不会被考虑。

iOS的保留循环[关于](循环引用)是一种情况,您(作为开发人员)负责计算引用并将其释放。


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