据我了解,如果没有其他东西“指向”该对象,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
a
、b
和 c
应该被垃圾回收,但它们都被其他对象引用。
Java 垃圾收集如何处理这个问题? (或者它只是内存消耗?)
如果无法通过从垃圾收集根开始的链访问对象,Java 的 GC 将其视为“垃圾”,因此将收集这些对象。即使对象可能相互指向形成一个循环,如果它们从根部被切断,它们仍然是垃圾。
有关血腥细节,请参阅 Java Platform Performance: Strategies and Tactics 中附录 A:垃圾收集的真相中有关不可访问对象的部分。
是的 Java 垃圾收集器处理循环引用!
How?
有称为垃圾收集根(GC 根)的特殊对象。这些总是可以访问的,任何以它们为根的对象也是如此。
一个简单的 Java 应用程序具有以下 GC 根:
main方法中的局部变量主线程主类的静态变量
https://i.stack.imgur.com/IjZqR.png
为了确定哪些对象不再使用,JVM 会间歇性地运行一种非常恰当地称为标记和清除算法的方法。它的工作原理如下
该算法遍历所有对象引用,从 GC 根开始,并将找到的每个对象标记为活动的。所有未被标记对象占用的堆内存都会被回收。它被简单地标记为空闲,基本上清除了未使用的对象。
因此,如果任何对象无法从 GC 根访问(即使它是自引用或循环引用的),它将受到垃圾回收。
当然,如果程序员忘记取消引用对象,有时这可能会导致内存泄漏。
https://i.stack.imgur.com/SZh5r.png
你是对的。您描述的垃圾收集的具体形式称为“引用计数”。在最简单的情况下,它的工作方式(至少从概念上讲,大多数现代的引用计数实现实际上是完全不同的),如下所示:
每当添加对对象的引用时(例如,将其分配给变量或字段,传递给方法等),其引用计数就会增加 1
每当删除对对象的引用时(方法返回,变量超出范围,字段被重新分配给不同的对象或包含该字段的对象自己被垃圾收集),引用计数减少 1
一旦引用计数达到 0,就不再有对该对象的引用,这意味着没有人可以再使用它,因此它是垃圾,可以被收集
而这个简单的策略正是你描述的问题:如果 A 引用 B 和 B 引用 A,那么它们的引用计数永远不会小于 1,这意味着它们永远不会被收集。
有四种方法可以解决这个问题:
忽略它。如果你有足够的内存,你的周期很小而且不频繁,而且你的运行时间很短,也许你可以不收集周期而侥幸逃脱。想一想 shell 脚本解释器:shell 脚本通常只运行几秒钟并且不会分配太多内存。将您的引用计数垃圾收集器与另一个没有循环问题的垃圾收集器结合起来。 CPython 这样做,例如:CPython 中的主要垃圾收集器是引用计数收集器,但有时会运行跟踪垃圾收集器来收集循环。检测周期。不幸的是,检测图中的循环是一项相当昂贵的操作。特别是,它需要与跟踪收集器几乎相同的开销,因此您也可以使用其中之一。不要以你我天真的方式实现算法:自 1970 年代以来,已经开发了多种非常有趣的算法,它们以一种巧妙的方式将循环检测和引用计数结合在一个操作中,这比任何一种都便宜得多单独或做一个跟踪收集器。
顺便说一句,实现垃圾收集器的其他主要方法(我已经在上面暗示过几次)是跟踪。跟踪收集器基于可达性 的概念。您从一些 根集 开始,您知道它们总是 可达(例如,全局常量,或 Object
类、当前词法范围、当前堆栈帧) 并从那里 trace 所有可从根集到达的对象,然后是从根集可到达的对象中可到达的所有对象,依此类推,直到获得传递闭包。在那个闭包中 not 的所有东西都是垃圾。
由于一个循环只能在其自身内部到达,而不能从根集中到达,因此它将被收集。
垃圾收集器从始终被认为“可达”的某些“根”位置集开始,例如 CPU 寄存器、堆栈和全局变量。它的工作原理是在这些区域中找到任何指针,然后递归地找到它们指向的所有内容。一旦找到了所有这些,其他一切都是垃圾。
当然,有很多变化,主要是为了速度。例如,大多数现代垃圾收集器都是“分代的”,这意味着它们将对象分成几代,并且随着对象变老,垃圾收集器在尝试确定该对象是否仍然有效的时间之间会变得越来越长——它只是开始假设,如果它已经活了很长时间,它很有可能会继续活得更久。
尽管如此,基本思想仍然是一样的:这一切都基于从一些它认为理所当然仍然可以使用的东西的根集开始,然后追逐所有的指针以找到其他可以使用的东西。
有趣的是:可能人们经常对垃圾收集器的这一部分与用于编组对象的代码之间的相似程度感到惊讶,例如远程过程调用。在每种情况下,您都从一些对象的根集开始,并追逐指针以找到那些引用的所有其他对象......
Java GC 实际上并不像您描述的那样运行。更准确地说,它们从一组基本对象开始,通常称为“GC 根”,并且会收集任何从根无法到达的对象。 GC 根包括以下内容:
静态变量
当前在正在运行的线程的堆栈中的局部变量(包括所有适用的“this”引用)
因此,在您的情况下,一旦局部变量 a、b 和 c 在您的方法结束时超出范围,就没有更多的 GC 根直接或间接包含对您的三个节点中的任何一个的引用,并且他们将有资格进行垃圾收集。
TofuBeer 的链接有更多详细信息,如果你想要的话。
This article(不再可用)深入了解垃圾收集器(概念上......有几种实现方式)。您帖子的相关部分是“A.3.4 Unreachable”:
A.3.4 不可达 当不再存在对它的强引用时,对象进入不可达状态。当一个对象不可达时,它是一个收集的候选对象。注意措辞:仅仅因为一个对象是收集的候选对象并不意味着它会立即被收集。 JVM 可以自由地延迟收集,直到立即需要对象消耗的内存。
垃圾收集通常并不意味着“如果没有其他东西'指向'该对象,则清理某个对象”(即引用计数)。垃圾收集大致意味着找到程序无法访问的对象。
因此,在您的示例中,在 a、b 和 c 超出范围后,它们可以被 GC 收集,因为您无法再访问这些对象。
比尔直接回答了你的问题。正如 Amnon 所说,您对垃圾收集的定义只是引用计数。我只是想补充一点,即使是非常简单的算法(如标记和扫描以及复制集合)也可以轻松处理循环引用。所以,没有什么神奇的!
GC 从 stack
或 permanent
的根(种子)开始标记 is used
标志。所有其他参考均未考虑在内。
iOS 的保留周期[About](循环引用)是您(作为开发人员)负责计算引用并取消分配它们的情况