ChatGPT解决这个技术问题 Extra ChatGPT

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 垃圾收集如何处理这个问题? (或者它只是内存消耗?)

请参阅:stackoverflow.com/questions/407855/…,特别是来自@gnud 的第二个答案。

H
Holger

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

有关血腥细节,请参阅 Java Platform Performance: Strategies and Tactics 中附录 A:垃圾收集的真相中有关不可访问对象的部分。


你有这方面的参考吗?很难测试它。
我添加了一个参考。您还可以覆盖对象的 finalize() 方法以找出它何时被收集(尽管这是我建议使用 finalize() 的唯一方法)。
只是为了澄清最后一条评论......在 finalize 方法中放置一个调试打印语句,打印出对象的唯一 ID。您将能够看到所有相互引用的对象都被收集起来。
“......足够聪明,可以识别......”听起来令人困惑。 GC 不必识别循环 - 它们只是无法访问,因此是垃圾
@tangens “你有这方面的参考吗?”在关于垃圾收集的讨论中。最好的。双关语。曾经。
A
Aniket Thakur

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

How?

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

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

main方法中的局部变量主线程主类的静态变量

https://i.stack.imgur.com/IjZqR.png

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

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

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

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

https://i.stack.imgur.com/SZh5r.png

来源:Java Memory Management


完美的解释!谢谢! :)
谢谢你链接那本书。它充满了关于这个和其他 Java 开发主题的重要信息!
在最后一张图片中,有一个不可到达的对象,但它在可到达的对象部分。
在最后一张图的“可达部分”中,仍有三个对象不可达。
J
Jörg W Mittag

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

每当添加对对象的引用时(例如,将其分配给变量或字段,传递给方法等),其引用计数就会增加 1

每当删除对对象的引用时(方法返回,变量超出范围,字段被重新分配给不同的对象或包含该字段的对象自己被垃圾收集),引用计数减少 1

一旦引用计数达到 0,就不再有对该对象的引用,这意味着没有人可以再使用它,因此它是垃圾,可以被收集

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

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

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

顺便说一句,实现垃圾收集器的其他主要方法(我已经在上面暗示过几次)是跟踪。跟踪收集器基于可达性 的概念。您从一些 根集 开始,您知道它们总是 可达(例如,全局常量,或 Object 类、当前词法范围、当前堆栈帧) 并从那里 trace 所有可从根集到达的对象,然后是从根集可到达的对象中可到达的所有对象,依此类推,直到获得传递闭包。在那个闭包中 not 的所有东西都是垃圾。

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


由于问题是特定于 Java 的,我认为值得一提的是 Java 不使用引用计数,因此不存在问题。 link to wikipedia 作为“进一步阅读”也会有所帮助。否则很棒的概述!
我刚刚阅读了您对 Jerry Coffin 帖子的评论,所以现在我不太确定 :)
J
Jerry Coffin

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

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

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

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


您所描述的是跟踪收集器。还有其他类型的收藏家。本次讨论特别感兴趣的是引用计数收集器,它确实倾向于在循环方面遇到问题。
@Jörg W Mittag:当然是真的——虽然我不知道使用引用计数的(合理的当前)JVM,所以它似乎不太可能(至少对我来说)它对原始问题有很大的不同。
@Jörg W Mittag:至少默认情况下,我相信 Jikes RVM 当前使用 Immix 收集器,这是一个基于区域的跟踪收集器(尽管它也使用引用计数)。我不确定您指的是引用计数,还是另一个使用引用计数而不进行跟踪的收集器(我猜是后者,因为我从未听说过 Immix 被称为“回收器”)。
我有点搞混了:Recycler 是(曾经?)在 Jalapeno 中实现的,我正在考虑的算法(曾经?)在 Jikes 中实现是 Ulterior Reference Counting。当然,尽管说 Jikes 使用这个或那个垃圾收集器是徒劳的,因为 Jikes 尤其是 MMtk 是专门为在同一个 JVM 中快速开发和测试不同的垃圾收集器而设计的。
Ulterior Reference Counting 是由 2007 年设计 Immix 的同一个人在 2003 年设计的,所以我猜后者可能会取代前者。 URC 是专门设计的,以便它可以与其他策略相结合,实际上 URC 论文明确提到 URC 只是通往结合了跟踪和引用计数优势的收集器的垫脚石。我猜 Immix 就是那个收藏家。无论如何,Recycler 是一个引用计数收集器,它仍然可以检测和收集循环:WWW.Research.IBM.Com/people/d/dfb/recycler.html
S
Sbodd

Java GC 实际上并不像您描述的那样运行。更准确地说,它们从一组基本对象开始,通常称为“GC 根”,并且会收集任何从根无法到达的对象。 GC 根包括以下内容:

静态变量

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

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

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


“......当前在一个正在运行的线程的堆栈中......”不是为了不破坏其他线程的数据而扫描所有线程的堆栈吗?
T
TofuBeer

This article(不再可用)深入了解垃圾收集器(概念上......有几种实现方式)。您帖子的相关部分是“A.3.4 Unreachable”:

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


direct link 到该部分
链接不再可用
A
Amnon

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

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


“垃圾收集大致意味着找到程序无法访问的对象”。在大多数 GC 算法中,实际上是相反的。你从 GC 根开始,看看你能找到什么,其余的被认为是未引用的垃圾。
引用计数是垃圾回收的两种主要实现策略之一。 (另一个是追踪。)
@Jörg:今天大多数时候,当人们谈论垃圾收集器时,他们指的是基于某种mark'n'sweep 算法的收集器。如果您没有垃圾收集器,通常会遇到引用计数。确实,ref 计数在某种意义上是一种垃圾收集策略,但如今几乎没有建立在它之上的 gc,所以说它是 gc 策略只会让人们感到困惑,因为实际上它不再是 gc策略,而是管理内存的另一种方法。
C
Claudiu

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


y
yoAlex5

[JVM Memory model diagram]

GC 从 stackpermanent 的根(种子)开始标记 is used 标志。所有其他参考均未考虑在内。

iOS 的保留周期[About](循环引用)是您(作为开发人员)负责计算引用并取消分配它们的情况