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

How does Java Garbage Collection work with Circular References?

根据我的理解,Java中的垃圾收集清理了一些对象,如果没有其他东西指向那个对象。

我的问题是,如果我们有这样的东西会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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垃圾回收是如何处理的?(还是仅仅是内存耗尽?)


Java的GC认为对象是"垃圾",如果它们不能通过从垃圾收集根开始的链来访问,那么这些对象将被收集。尽管对象可能互相指向以形成一个循环,但如果从根目录中切断它们,它们仍然是垃圾。

请参阅附录A中关于不可达对象的部分:Java平台性能中垃圾收集的真相:血腥细节的策略和策略。


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

1
How?

有称为垃圾收集根(GC根)的特殊对象。它们总是可以到达的,任何根上有它们的对象也是如此。

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

  • 主方法中的局部变量
  • 主线
  • 主类的静态变量
  • enter image description here

    为了确定哪些对象不再使用,JVM间歇性地运行一种非常合适的标记和扫描算法。工作原理如下

  • 该算法从GC开始遍历所有对象引用根,并标记每一个被发现的活着的物体。
  • 所有未被标记对象占用的堆内存都是回收的它被简单地标记为自由,基本上是自由的未使用的对象。
  • 因此,如果从GC根无法访问任何对象(即使它是自引用的或循环引用的),它将受到垃圾收集的影响。

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

    enter image description here

    来源:Java内存管理


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

    当然,有相当多的变化,主要是为了速度。例如,大多数现代垃圾收集器都是"世代"的,这意味着它们将对象分为几代,随着对象变老,垃圾收集器在试图确定该对象是否仍然有效的时间之间会变得越来越长--它只是开始假设,如果它已经活了很长时间,则可能是pr很好,它会继续活得更长。

    尽管如此,基本的思想仍然是一样的:它都是基于从一些它认为理所当然的仍然可以使用的东西的根集合开始的,然后追踪所有的指针来找到其他的可以使用的东西。

    有趣的是:也许人们经常会惊讶于垃圾收集器的这一部分与为诸如远程过程调用之类的事情封送对象的代码之间的相似程度。在每种情况下,您都是从一些根对象集开始的,并跟踪指针以查找所有其他引用的对象…


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

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

    这个简单的策略有你描述的问题:如果A引用B和B引用A,那么它们的两个引用计数都不能小于1,这意味着它们永远不会被收集。

    解决这个问题有四种方法:

  • 忽略它。如果您有足够的内存,那么您的周期很小,而且不频繁,并且运行时间很短,那么您可能只需不收集周期就可以摆脱它。想想一个shell脚本解释器:shell脚本通常只运行几秒钟,并且不会分配太多内存。
  • 将引用计数垃圾收集器与另一个循环没有问题的垃圾收集器结合起来。例如,cpython这样做:cpython中的主垃圾收集器是一个引用计数收集器,但有时运行跟踪垃圾收集器来收集循环。
  • 检测循环。不幸的是,在图中检测循环是一个相当昂贵的操作。特别是,它需要与跟踪收集器几乎相同的开销,所以您也可以使用其中之一。
  • 不要像你和我那样天真地实现算法:自20世纪70年代以来,已经开发了多个非常有趣的算法,它们以一种巧妙的方式将循环检测和引用计数结合在一起,这比单独执行或执行跟踪收集器都要便宜得多。
  • 顺便说一下,实现垃圾收集器的另一个主要方法(我已经在上面几次暗示过这一点)是跟踪。跟踪收集器基于可达性的概念。从已知的始终可访问的某个根集开始(例如全局常量或Object类、当前词法范围、当前堆栈帧),然后从那里跟踪从根集可访问的所有对象,然后跟踪从根集可访问的对象可访问的所有对象,依此类推,unt如果你有可传递的闭包。所有不在那个封闭区域的东西都是垃圾。

    因为一个循环只能在其自身内访问,而不能从根集访问,所以将收集它。


    Java GCS并不像您描述的那样实际操作。更准确的说法是,它们从一组基本的对象(通常称为"gc根")开始,并收集从根无法访问的任何对象。GC根包括以下内容:

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

    因此,在您的例子中,一旦局部变量A、B和C超出了方法末尾的范围,就没有更多的GC根直接或间接地包含对您的三个节点中任何一个的引用,它们将符合垃圾收集的条件。

    如果你想要的话,ToSubeer的链接有更多的细节。


    本文(不再可用)深入介绍了垃圾收集器(概念上…有几个实现)。你文章的相关部分是"A.3.4不可访问":

    A.3.4 Unreachable An object enters an unreachable state when no more
    strong references to it exist. When an object is unreachable, it is a
    candidate for collection. Note the wording: Just because an object is
    a candidate for collection doesn't mean it will be immediately
    collected. The JVM is free to delay collection until there is an
    immediate need for the memory being consumed by the object.


    比尔直接回答了你的问题。正如Amnon所说,垃圾收集的定义只是引用计数。我只是想补充一点,即使是像mark和sweep以及copy collection这样非常简单的算法,也可以轻松地处理循环引用。所以,没有什么神奇的!


    垃圾收集通常并不意味着"清理某个对象,如果没有其他对象指向该对象"(这是引用计数)。垃圾收集大致是指查找程序无法访问的对象。

    所以在您的示例中,在A、B和C超出范围之后,GC可以收集它们,因为您不能再访问这些对象了。