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

根据我的理解,如果没有其他东西“指向”那个对象,那么 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 垃圾回收如何处理这个问题? (或者它只是一个内存消耗?)

59246 次浏览

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

请参阅附录 A 中关于不可达对象的部分: Java 平台性能: 策略与策略中关于垃圾收集的真相以获得详细信息。

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

因此在您的示例中,在 a、 b 和 c 超出作用域之后,它们可以由 GC 收集,因为您不能再访问这些对象了。

本文 (不再可用)深入探讨了垃圾收集器(概念上... 有几种实现)。你的帖子的相关部分是“ A3.4无法到达”:

答3.4不可及物体进入不可及状态时,没有更多 存在对它的强引用。当一个对象不可访问时,它是一个 注意措辞: 仅仅因为一个对象是 一个收集的候选人并不意味着它会立即 JVM 可以自由地延迟收集,直到有一个 对被对象消耗的内存的即时需求。

Java GC 实际上并不像您描述的那样运行。更准确的说法是,它们从基本对象集(通常称为“ GC 根”)开始,并将收集无法从根访问的任何对象。
GC 根包括以下内容:

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

因此,在您的示例中,一旦本地变量 a、 b 和 c 在方法结束时超出作用域,就不再有直接或间接包含对三个节点中任何一个节点的引用的 GC 根,它们将有资格进行垃圾收集。

如果你想要的话,豆腐啤酒的链接有更多的细节。

比尔直接回答了你的问题。正如 Amnon 所说,您对垃圾收集的定义只是引用计数。我只是想补充说,即使是非常简单的算法,如标记和扫描和复制收集很容易处理循环引用。所以,没什么神奇的!

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

当然,有很多变化,主要是为了速度。例如,大多数现代垃圾收集器都是“分代”的,这意味着它们将对象分成几代,当一个对象变得越来越老时,垃圾收集器在试图确定该对象是否仍然有效的时间间隔越来越长——它只是开始假设,如果它存在了很长一段时间,那么很有可能它将继续存在更长的时间。

尽管如此,其基本思想仍然是相同的: 它完全基于从一些它认为理所当然的事物的根集开始,这些事物仍然可以被使用,然后追踪所有的指针,以找到其他可以被使用的东西。

有趣的是: 垃圾收集器的这一部分与用于封送对象(例如远程过程调用)的代码之间的相似程度可能会让人们感到惊讶。在每种情况下,您都是从一些对象的根集开始,并通过追踪指针来查找所有其他引用的对象..。

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

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

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

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

  1. 别理它。如果您有足够的内存,您的周期很小且不频繁,运行时很短,也许您可以简单地不收集周期。想象一下 shell 脚本解释器: shell 脚本通常只运行几秒钟,不会分配太多内存。
  2. 将引用计数垃圾收集器与 另一个垃圾收集器组合起来,后者没有循环问题。例如,CPython 就是这样做的: CPython 中的主垃圾收集器是一个引用计数收集器,但是有时会运行一个跟踪垃圾收集器来收集循环。
  3. 检测周期。不幸的是,在图中检测循环是一个相当昂贵的操作。特别是,它所需的开销与跟踪收集器所需的开销几乎相同,因此您也可以使用其中之一。
  4. 不要用你我天真的方式来实现这个算法: 自20世纪70年代以来,已经有很多非常有趣的算法被开发出来,它们以一种聪明的方式将循环检测和引用计数结合在一个操作中,这种方式明显比两者分开做或者做一个跟踪收集器要便宜得多。

顺便说一下,其他实现垃圾收集器的主要方法(我在上面已经提到过几次了)是 追踪。跟踪收集器基于 平易近人的概念。你从一些你知道是 一直都是可达的 根集开始(例如全局常量,或者 Object类,当前的词法范围,当前的堆栈帧) ,然后你从根集中可达到的所有对象 Trace,然后从根集中可达到的所有对象以此类推,直到你有了传递闭包。闭包中的 没有都是垃圾。

由于循环只能在其内部访问,而不能从根集访问,因此将收集它。

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

How?

有一些称为垃圾收集根(GC 根)的特殊对象。它们总是可以访问的,任何将它们放在自己根目录下的对象也是如此。

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

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

enter image description here

为了确定哪些对象不再使用,JVM 间歇性地运行所谓的 标记-扫描算法。它的工作原理如下

  1. 该算法从 GC 开始遍历所有对象引用 根,并标记每一个物体发现活着。
  2. 所有未被标记对象占用的堆内存都是 它被简单地标记为免费的,基本上是免费的 未使用的物品。

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

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

enter image description here

资料来源: Java 内存管理

[ JVM 内存模型图]

GC 从 stackpermanent的根(种子)开始用 is used标记。所有其他的参考文献都没有考虑到。

Retain cycle< sup > [ About ] (循环引用) for iOS 是这样一种情况,即您(作为开发人员)负责计算引用并释放它们