垃圾回收 - 根节点

13

我最近阅读了关于垃圾回收的一些文章(主要是针对Java),但仍有一个问题未解答:JVM(或运行时系统)如何跟踪当前活动对象?

我理解这些对象是当前在堆栈上的对象,即所有局部变量或函数参数,这些都是对象。但这种方法的问题在于,每当运行时系统检查堆栈上的当前内容时,它如何区分引用变量和简单的整型变量?它不能,对吧?

因此,必须有一种机制允许运行时系统构建初始的活动对象列表,以便传递给标记-清除阶段。

3个回答

7
我发现greyfairer提供的答案是错误的。JVM运行时不会通过查看使用哪些字节码将数据推送到堆栈来收集根集。堆栈帧由4个字节(32位架构)插槽组成。每个插槽可以是指向堆对象或原始值(如int)的引用。当需要进行GC时,运行时从上到下扫描堆栈。对于每个插槽,如果它包含引用,则:

a. 它在4字节边界上对齐。

b. 插槽中的值指向堆区域(在下限和上限之间)。

c. 分配位设置。分配位是一个标志,指示与其相应的内存位置是否已分配。

这是我的参考资料:http://www.ibm.com/developerworks/ibm/library/i-garbage2/

还有一些其他技术可以找到根集(不是在Java中)。例如,因为指针通常在4/8字节边界上对齐,所以第一位可以用于指示插槽是原始值还是指针:对于原始值,第一位设置为1。这样做的缺点是您只有31位(32位架构)来表示整数,并且对原始值的每个操作都涉及移位,这显然是一个开销。

此外,您可以使所有类型(包括int)都在堆上分配。也就是说,所有东西都是对象。然后,堆栈帧中的所有插槽都是引用。


总的来说,这是相当低级别的差异,而不是JVM?但是JVM为字节码声明了一个引用类型,为什么不使用它呢?你确定这是如此低级别,而不是在字节码级别上? - Bober02
1
据我所知(基于我之前给出的链接和浏览几个JVM实现的代码),我确信我的理解是正确的。你可以简单地深入一些开源JVM实现的GC代码来检查这一点。它们都需要遍历堆栈以查找引用。但是,也许用于验证一个插槽是否为引用的标准略有不同(大多数验证a和b,对于c,它真的基于实现)。 - Rainfield
为什么不使用字节码,这是我的理解(不确定是否正确)。GC 是运行时的事情,但字节码是在编译时生成的并且是静态的。当 GC 发生时,运行时系统需要找到根并跟随它们以查找活动对象。为了做到这一点,您必须实际检查每个堆栈帧槽中的值,即使您知道此槽在编译时包含引用(如 greyfairer 所说,您可以通过查看字节码来知道这一点)。因为您需要知道确切的引用值才能找到堆中的其他对象。 - Rainfield
那么为什么要检查字节码呢?反正你还是得遍历堆栈。 - Rainfield
分配位allocbit位于哪里?如果它在对象之外的某个地方,你需要增加分配开销(只需一次操作,但这很重要)。如果它在对象内部,则可能会将其他数据误解为allocbit并遇到底部此文章中提到的问题。 - maaartinus
@maaartinus 好吧,如果它与Java有关,那么现在已经失效的链接肯定描述了IBM的JVM。既然我们已经知道它从未适用于HotSpot JVM,唯一剩下的问题就是它是否仍适用于IBM的JVM。这就是当我们询问“Java”实际上是关于JVM实现特定行为时得到的答案。 - Holger

2
运行时可以完美地区分引用变量和基本数据类型,因为这是在编译后的字节码中确定的。
例如,如果函数f1调用函数f2(int i, Object o, long l),则调用函数f1将在堆栈(或寄存器)上推送4字节表示i,4(或8?)字节用于o的引用,并且8字节用于l。被调用的函数f2知道在堆栈上找到这些字节的位置,并且可能会复制指向堆上某个对象的引用,也可能不会。当函数f2返回时,调用函数将从堆栈中删除参数。
运行时解释字节码并记录它推送或从堆栈中删除的内容,因此它知道什么是引用和什么是原始值。
根据http://www.javacoffeebreak.com/articles/thinkinginjava/abitaboutgarbagecollection.html,Java使用追踪垃圾收集器而不是引用计数算法。

谢谢您的回答。有了这个想法,当JVM启动垃圾收集时,它是如何进行的?它实际上如何定位根节点 - 是跳回堆栈还是有一个单独的节点集合? - Bober02
请参考文章链接进行深入剖析。 - GeertPt
我在你提到的文章中发现了以下句子:“标记和清除遵循从堆栈和静态存储开始并跟踪所有句柄以查找活动对象的相同逻辑。”他们所指的神秘句柄是什么? - Bober02
对我来说,句柄、指针和引用都是一样的。这意味着运行时确实会保留一个列表,其中包含指向堆上对象的引用/指针的栈位置,并从那里找到指向其他被这些对象引用的对象的指针,以此类推... - GeertPt
啊,好的,那么就是使用了辅助数据结构...这很有道理! - Bober02
解释器的工作并不重要,因为代码的大部分都被编译成机器语言。所以你的解决方案并不能解决问题。 - maaartinus

1
热点虚拟机为每个编译的子程序生成一个GC映射,其中包含有关根的信息。例如,假设它已将一个子程序编译成机器码(字节码的原理相同),其长度为120个字节,则其GC映射可能如下所示:
0 : [RAX, RBX]
4 : [RAX, [RSP+0]]
10 : [RBX, RSI, [RSP+0]]
...
120 : [[RSP+0],[RSP+8]]

在这里,[RSP+x] 表示堆栈位置,R?? 表示寄存器。因此,如果线程在偏移量为10的汇编指令处停止,并且运行了 gc 循环,则 HotSpot 知道三个根位于 RBXRSI[RSP+0] 中。它跟踪这些根并更新指针,如果必须移动对象的话。
我描述的 GC 映射格式只是为了演示原理,显然不是 HotSpot 实际使用的格式。它不完整,因为它不包含有关包含基元实值的寄存器和堆栈插槽的信息,而且对于每个指令偏移量使用列表不是空间有效的。有许多方法可以以更高效的方式打包信息。

这张地图仅在安全点而非任意偏移处需要(这可能是0、4和10之间存在间隙的原因)。我刚刚发现了这篇文章支持你的答案。 - maaartinus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接