Java垃圾收集器如何处理自引用?

23

希望这是一个简单的问题。

以循环链表为例:

class ListContainer
{
  private listContainer next;
  <..>

  public void setNext(listContainer next)
  {
    this.next = next;
  }
}

class List
{
  private listContainer entry;
  <..>
}

现在由于它是一个循环链接列表,当添加单个元素时,它的下一个变量中有对自身的引用。当删除列表中唯一的元素时,条目(entry)设置为null。是否需要将ListContainer.next也设置为null,以便垃圾回收器(Garbage Collector)释放它的内存或者它自动处理这样的自我引用?

7个回答

24

仅依赖于引用计数的垃圾收集器通常容易在回收类似这样的自我引用结构时出现问题。这些垃圾收集器依靠对象被引用的次数来计算一个给定对象是否可达。

非引用计数方法使用更全面的可达性测试来确定对象是否有资格进行垃圾收集。这些系统定义了一组始终被认为是可达的对象(或对象集)。从这个对象图中可获得引用的任何对象都被认为不符合收集条件。而直接无法从该对象访问的任何对象则相反。因此,循环并不会影响可达性,并且可被收集。

另请参阅维基百科关于追踪式垃圾收集器的页面。


任何带有循环检测器(例如试验删除或标记/扫描)的引用计数垃圾收集器也会捕获自引用。 - Luke Quinane
一个处理循环引用的引用计数垃圾回收器的例子是CPython。一个处理不了循环引用的例子是Visual Basic 6。 - Nate C-K

14
循环引用是一个(可解决的)问题,如果您依赖于计数引用来决定对象是否已死亡。据我所知,没有一个Java实现使用引用计数。新的Sun JRE使用了几种类型的GC,全部都是标记-扫描或复制算法。
您可以在维基百科上了解更多有关垃圾收集的一般信息,也可以在这里这里 阅读一些关于Java GC的文章。

1
Jikes RVM 包含高性能的引用计数 GC。详见 http://cs.anu.edu.au/~Steve.Blackburn/pubs/papers/urc-oopsla-2003.pdf。 - Luke Quinane

7
实际答案取决于实现。Sun JVM跟踪一些根对象(线程等),当需要进行垃圾收集时,追踪从这些对象可达的对象并保存它们,丢弃其余对象。为了允许一些优化,实际情况比这更复杂。此版本不关心循环引用:只要没有活动对象保留对死对象的引用,就可以进行垃圾回收。
其他JVM可能使用称为引用计数的方法。当创建对象的引用时,某个计数器会递增,当引用超出范围时,计数器会递减。如果计数器达到零,对象将被终结和垃圾回收。但是,此版本允许永远无法进行垃圾回收的循环引用的可能性。为了安全起见,许多此类JVM包括备用方法,定期运行以确定哪些对象实际上已经死亡,并解决自我引用和碎片整理堆的问题。

6
作为一个非正式的回答(现有的答案已经足够),如果你对垃圾收集(GC)感兴趣,你可能想查看一份关于JVM垃圾收集系统的白皮书。(任何一个,只需谷歌JVM Garbage Collection)
我对一些技术使用方法感到惊讶,当阅读一些概念如“Eden”时,我真正意识到Java和JVM实际上可以在速度上击败C/C++。(每当C/C++释放对象/内存块时,都需要涉及代码...当Java释放对象时,实际上什么都不做;因为在良好的面向对象编程中,大多数对象几乎立即被创建和释放,这非常高效。)
现代GC往往非常高效,管理旧对象和新对象的方式非常不同,能够控制GC短而粗略或长而彻底,许多GC选项可以通过命令行开关进行管理,因此了解所有术语实际上是有用的。
注意:我刚意识到这是误导性的。C++的堆栈分配非常快--我的观点是关于分配能够在当前程序完成后继续存在的对象(我认为应该是所有对象--如果您要考虑OO,这是您不必考虑的事情,但在C++中速度可能使这不切实际)。
如果您只在堆栈上分配C++类,则其分配速度至少与Java相同。

你好!您能否详细解释一下您所说的“当Java释放一个对象时,实际上它什么也不做;因为在良好的面向对象编程中,大多数对象几乎立即被创建和释放”是什么意思?谢谢 :) - rinogo
GC区的第一部分被分成两个称为“伊甸园”的部分。每次只使用其中的1/2。它只是从底部到顶部填充。当该区域已满时,已释放的任何内容在此时都已完全遗忘 - 剩下的任何内容都将复制到另一半中,然后第一半被视为空,并且另一半开始填充。当一个对象看起来会持续一段时间时,Java将其从伊甸园移动到另一个区域,并使用完全不同的机制来跟踪它。 - Bill K
这意味着对于小对象的分配和释放与 C 语言的堆栈分配大致相同,也没有特定的“释放”步骤。Java 的好处在于相同的机制适用于短期和长期对象...运行时会找出何时使用哪种方法,并分析代码以尽快确定应该放置在哪里。 GC 还可以通过命令行选项进行精细调整。 - Bill K
参考资料,这种GC被称为停止-复制。说在使用停止-复制GC时分配和释放的成本与堆栈相同(仅增加计数器,释放是无操作)是误导性的。GC增加了隐藏的成本:偶尔中断程序,从旧Eden的根递归地跟随指针,将活动对象复制到新的Eden,并更新所有指向它们的指针。这些收集阶段每当您的Eden一半满时发生。您分配的越多,触发它的次数就越多。它比在堆栈上分配更昂贵。 - Maëlan

4

Java会收集所有不可达的对象。如果没有其他东西引用该条目,那么它将被收集,即使它对自身有引用。


那么您的意思是未被引用的对象“new ListContainer()”将会被立即回收吗?PS:ListContainer类来自问题的代码。 - Han XIAO
通常,类似 new ListContainer() 的表达式将被评估,并且在引用仍然在堆栈上的同时,将被分配给变量或作为参数传递给方法,并因此在一段时间内被引用。如果(或当)它实际上不再被引用,则可能在未来被收集。不能保证它会立即发生,但只会在 JVM 用尽内存之前发生。 - Dave L.

4
是的,Java垃圾回收器可以处理自引用!
How?

有一些特殊的对象被称为垃圾收集根(GC roots)。它们始终是可达的,因此任何具有这些对象作为其根的对象也是可达的。
一个简单的Java应用程序具有以下GC roots:
  1. 主方法中的局部变量
  2. 主线程
  3. 主类的静态变量
为了确定哪些对象不再使用,JVM会间歇性地运行非常恰当地称为“标记-清除算法”的算法。它的工作方式如下:
  1. 该算法遍历所有对象引用,从GC roots开始,并将找到的每个对象标记为活动状态。
  2. 所有未被标记对象占用的堆内存都可以被回收。它只是被标记为空闲,实质上被清除了未使用的对象。
因此,如果任何对象无法从GC roots访问(即使它是自引用或循环引用),它都将被垃圾回收。

2
简单来说,是的。 :)
请查看http://www.ibm.com/developerworks/java/library/j-jtp10283/
所有JDK(来自Sun)都有一个“可达性”的概念。如果GC无法“到达”对象,则该对象将消失。
这并不是什么“新”的信息(你前面两个回答者已经很好地解答了),但该链接很有用,而且简洁明了。 :)

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