Java垃圾回收如何与循环引用一起使用?


161

根据我的理解,如果没有其他“指向”该对象的内容,则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
请参阅:stackoverflow.com/questions/407855/…,特别是@gnud的第二个答案。
赛斯

Answers:


161

如果无法通过从垃圾回收根开始的链访问对象,Java的GC会将其视为“垃圾”,因此将收集这些对象。即使对象可能指向彼此以形成一个循环,但如果将它们与根切断,它们仍然是垃圾。

有关详细信息,请参阅附录A:《Java平台性能中的垃圾收集的真相:策略和策略》中有关不可访问对象的部分。


14
你有参考吗?很难测试。
tangens

5
我添加了参考。您还可以重写对象的finalize()方法以查找何时收集对象(尽管这是我建议使用finalize()的唯一建议)。
比尔蜥蜴

1
只是为了澄清最后一条评论...将调试打印语句放在finalize方法中,该方法为对象打印出唯一的ID。您将能够看到所有互相引用的对象被收集。
比尔蜥蜴

4
“ ...足够智能以识别...”听起来令人困惑。GC没有认识到循环-他们只是无法到达,因此垃圾
亚历山大Malakhov

86
@tangens“您对此有参考吗?” 在有关垃圾收集的讨论中。最好。双关语 曾经
米哈尔Kosmulski

139

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

How?

有一些特殊的对象称为垃圾收集根(GC根)。它们始终是可访问的,因此拥有它们自己根目录的任何对象也是如此。

一个简单的Java应用程序具有以下GC根目录:

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

在此处输入图片说明

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

  1. 该算法从GC根开始遍历所有对象引用,并将找到的每个对象标记为活动。
  2. 回收未被标记对象占用的所有堆内存。它被简单地标记为空闲,基本上清除了未使用的对象。

因此,如果从GC根目录无法访问任何对象(即使它是自引用的或循环引用的),也将对其进行垃圾回收。

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

在此处输入图片说明

来源:Java内存管理


3
完美的解释!谢谢!:)
Jovan Perovic

感谢您链接那本书。它包含有关此主题和其他Java开发主题的重要信息!
Droj '16

14
在上一张图片中,有一个不可访问的对象,但在“可访问对象”部分中。
La VloZ Merrill

13

垃圾收集器从始终被视为“可访问”的某些“根”位置开始,例如CPU寄存器,堆栈和全局变量。它的工作原理是找到这些区域中的任何指针,然后递归地找到它们指向的所有内容。一旦找到所有这些,其他所有东西都是垃圾。

当然,有很多变化,主要是为了提高速度。例如,大多数现代垃圾收集器都是“世代的”,这意味着它们将对象分为几代,并且随着对象的变老,垃圾收集器在试图弄清楚该对象是否仍然有效的时间之间的间隔越来越长。 -它只是开始假设,如果它已经存在了很长一段时间,那么很有可能会持续更长的寿命。

尽管如此,基本思想仍然是相同的:它的基础是从一些仍然被认为是理所当然的东西的根源开始,然后追逐所有指针以寻找其他可以使用的东西。

除了有趣之外:垃圾收集器的这一部分与用于编组对象(如远程过程调用)的代码之间的相似程度,可能使人们经常感到惊讶。在每种情况下,您都是从一组对象的根开始,并追逐指针以查找所有其他引用的对象...


您所描述的是一个跟踪收集器。还有其他种类的收藏家。对于本次讨论特别感兴趣的是引用计数收集器,它确实存在循环方面的麻烦。
约尔格W¯¯米塔格

@JörgW Mittag:确实是这样-尽管我不知道使用引用计数的(合理的最新)JVM,所以(至少对我来说)这不太可能对原始问题产生很大的影响。
杰里·科芬

@JörgW Mittag:至少默认情况下,我相信Jikes RVM当前使用Immix收集器,该收集器是基于区域的跟踪收集器(尽管它也使用引用计数)。我不确定您是在引用该引用计数,还是在使用另一个没有跟踪的引用计数的收集器(我猜是后者,因为我从未听说过Immix正在调用“回收站”)。
杰里·科芬,

我有点困惑:Recycler是在Jalapeno中实现的,是我正在考虑的算法,Jikes是Ulterior Reference Counting是实现的。当然,Atlhough说,Jikes使用此垃圾回收器还是徒劳的,因为Jikes特别是MMtk是专门为在同一JVM中快速开发和测试不同的垃圾回收器而设计的。
约尔格W¯¯米塔格

2
Ulterior Reference Counting是由2007年设计Immix的同一人于2003年设计的,因此我想后者可能会取代前者。URC经过专门设计,可以与其他策略结合使用,实际上,URC论文明确提到,URC只是迈向收集者的垫脚石,结合了跟踪和引用计数的优点。我猜Immix是那个收藏家。无论如何,回收站是一个参考计数收集器,其仍然可以检测并收集循环:WWW.Research.IBM.Com/people/d/dfb/recycler.html
约克W¯¯米塔格

13

你是对的。您描述的垃圾收集的特定形式称为“ 引用计数 ”。在最简单的情况下,它的工作方式(从概念上讲,至少,大多数现代的引用计数实现实际上是完全不同的),如下所示:

  • 每当添加对对象的引用时(例如,将其分配给变量或字段,传递给方法等),其引用计数都将增加1
  • 每当删除对对象的引用(方法返回,变量超出范围,将字段重新分配给其他对象或包含该字段的对象本身被垃圾回收)时,引用计数都会减少1
  • 一旦引用计数达到0,就不再有对该对象的引用,这意味着没有人可以使用它,因此它是垃圾并且可以被收集

这个简单的策略确实存在您所描述的问题:如果A引用B和B引用A,则它们的两个引用计数都不能小于1,这意味着它们将永远不会被收集。

有四种方法可以解决此问题:

  1. 忽略它。如果您有足够的内存,则周期较小且不频繁,并且运行时间较短,也许您可​​以不用收集周期而摆脱困境。考虑一下shell脚本解释器:shell脚本通常只运行几秒钟,并且不会分配太多内存。
  2. 将您的引用计数垃圾收集器与另一个没有循环问题的垃圾收集器结合使用。例如,CPython这样做:CPython中的主要垃圾收集器是引用计数收集器,但是有时会运行跟踪垃圾收集器来收集周期。
  3. 检测周期。不幸的是,检测图中的周期是相当昂贵的操作。特别是,它需要与跟踪收集器几乎相同的开销,因此您也可以使用其中之一。
  4. 不要以您和我的天真方式实现该算法:自1970年代以来,已经开发了多种非常有趣的算法,这些算法以一种巧妙的方式将周期检测和引用计数相结合的巧妙方法,比两种方法都便宜得多既可以单独进行,也可以进行跟踪收集。

顺便说一句,实现垃圾收集器的另一种主要方法(我已经在上面暗示过两次)是跟踪。跟踪收集器基于可达性的概念。首先从一些您知道总是可以到达的根集开始(例如,全局常量或类,当前词法作用域,当前堆栈框架),然后从那里跟踪从根集可到达的所有对象,然后从根集合可访问的对象到所有可访问的对象,依此类推,直到您具有传递闭包为止。一切,这是不是在封闭的垃圾。Object

由于一个循环只能在自身内部到达,而不能从根集中到达,因此它将被收集。


1
由于问题是特定于Java的,因此我认为值得一提的是Java不使用引用计数,因此不存在问题。另外链接到Wikipedia将有助于“进一步阅读”。否则很棒的概述!
亚历山大·马拉霍夫

我刚刚读过您对杰里·科芬(Jerry Coffin)帖子的评论,所以现在我不确定:)
亚历山大·马拉霍夫

8

Java GC实际上并不像您描述的那样运行。准确地说,它们是从一组基础对象开始的,这些对象通常被称为“ GC根目录”,并将收集从根目录无法访问的任何对象。
GC根目录包括:

  • 静态变量
  • 当前正在运行的线程堆栈中的局部变量(包括所有适用的“ this”引用)

因此,在您的情况下,一旦局部变量a,b和c在方法末尾超出范围,就不再有GC根直接或间接包含对您三个节点中任何一个的引用,并且他们将有资格进行垃圾收集。

如果需要,TofuBeer的链接有更多详细信息。


“ ...当前位于正在运行的线程的堆栈中...”不是为了不破坏其他线程的数据而扫描所有线程的堆栈吗?
亚历山大·马拉霍夫

6

本文(不再提供)深入介绍了垃圾收集器(概念上,有几种实现方式)。您的帖子的相关部分是“ A.3.4无法访问”:

A.3.4不可达对象当不再存在对它的强引用时,该对象将进入不可达状态。当对象不可访问时,它是收集的候选对象。请注意以下措辞:仅仅因为对象是收集对象而已,并不意味着将立即收集该对象。JVM可以自由地延迟收集,直到立即需要该对象消耗的内存为止。



1
链接不再可用
titus

1

垃圾回收通常并不意味着“如果没有其他东西“指向”该对象,则清理某些对象”(即引用计数)。垃圾收集大致意味着找到无法从程序访问的对象。

因此,在您的示例中,a,b和c超出范围后,它们可以由GC收集,因为您无法再访问这些对象。


“垃圾收集大致意味着找到程序无法达到的对象”。在大多数GC算法中,实际上是相反的。您从GC根目录开始,看看可以找到什么,其余的则视为未引用的垃圾。
Fredrik

1
引用计数垃圾收集的两种主要实现策略之一。(另一个正在跟踪。)
JörgW Mittag

3
@Jörg:今天的大多数时候,当人们谈论垃圾收集器时,他们指的是基于某种'mark'n'sweep算法的垃圾收集器。如果没有垃圾收集器,引用计数通常就是您所坚持的。的确,引用计数在某种意义上是一种垃圾回收策略,但今天几乎没有基于它的gc,因此说这是gc策略只会使人们感到困惑,因为在实践中它不再是gc策略,而是管理内存的另一种方法。
Fredrik

1

比尔直接回答了您的问题。正如阿姆农所说,您对垃圾收集的定义只是引用计数。我只是想补充一点,即使是非常简单的算法(例如标记,清除和复制集合)也可以轻松处理循环引用。所以,这没什么神奇的!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.