如果 JVM 在进行 GC 时不断移动对象,它如何解析引用?

Posted

技术标签:

【中文标题】如果 JVM 在进行 GC 时不断移动对象,它如何解析引用?【英文标题】:If the JVM keeps moving objects around when it does GC, how does it resolve references? 【发布时间】:2012-03-16 23:17:00 【问题描述】:

我正在阅读有关 JVM 调优的内容,我突然想到 JVM 在执行 GC 时会不断移动对象。但是 Java 对象之间存在相互引用,人们会认为这是作为指针实现的,但是 JVM 不可能在每次移动对象后遍历整个堆,并更新所有引用;这肯定会永远持续下去。那么,如果引用没有改变,但对象的物理位置发生了变化,它如何解析引用呢?

我已经阅读了很多关于 JVM 的文章,但从未在任何地方解释过,甚至没有暗示过。

[编辑] 我的意思是引用是单向的。从指针指向指向是“瞬时的”,但反过来需要完整的堆扫描。虽然有可能,但似乎不太可能。如果 10K 对象在一次次要收集中幸存下来,那么进行 10K 次全堆扫描以更新对这些对象的引用需要多长时间?必须使用某种优化的算法或结构。

【问题讨论】:

类似问题:***.com/questions/88852/… 【参考方案1】:

如果您真的对垃圾收集器的工作原理感兴趣,我可以推荐 Richard Jones 的 2 本书关于垃圾收集的书籍。链接/参考是here。这不是专门针对 Java 垃圾回收的。

(我有一本旧书,新书在我的购物清单上。)


以下是复制收集器如何处理此问题的简单版本。

复制收集器的工作原理是将对象从一个空间(源空间)复制到另一个空间(目标空间)。

具体来说,GC 从每个 GC 根开始遍历“from”空间内的可达对象图。每次它找到对节点的引用(在实例字段、静态字段、堆栈帧等中)时,它都会检查引用指向的对象是否已被标记为已访问。

如果尚未标记,GC 会执行以下操作:

    它在从空间中标记对象。 它将对象复制到目标空间中。 它将对象的地址存储到空间对象的空间中。 (这就像一个转发地址。) 它递归地访问对象的目标空间副本的每个引用字段。

这个对空间对象的引用的结果。

如果对象已经被标记,GC 会查找转发地址,并返回。

然后用指向空间中对象的指针更新 GC 获取引用的位置(在 to-space,或某个 GC 根中)。

如果您遵循所有这些,那么您将看到 GC 不需要去寻找所有持有对给定移动对象的引用的位置。相反,它只是遇到可到达对象的遍历中的所有位置。当然,GC确实必须进行这种遍历,但是有多种技术可以减少每个 GC 循环中需要完成的遍历量。

如果您还没有遵循上述内容,请阅读我推荐的一本教科书。他们会比我做得更好。您还可以找到有关其他类型的 GC 如何处理此问题的资料。


Java HotSpot GC 是所有一种或另一种形式的复制收集器。对于并行和并发收集,事情比我上面描述的要复杂一些,但是“转发地址”机制对所有这些都是通用的。

(关于 HotSpot GC 的已发表论文或其他公开文档并不多,并且现有的大多数材料都假设读者对现代垃圾收集器的工作方式有很好的理解。)

【讨论】:

但是如果老年代中有对象引用了新生代中的对象(正在被移动)怎么办?你必须遍历整个老一代来寻找对任何被移动的东西的引用。让每个引用都通过一个保留对象实际位置的间接层似乎会更有效。 那是 GC 特定的。但一般的做法是,JVM 在更新对象中的指针字段时执行“写屏障”序列。写屏障负责记录老->年轻代指针。 (例如 G1 收集器使用“cards”和“remembered sets”来做到这一点。) 所以,诸位:当对象在伊甸区时,局部变量中的对象引用会在 gc 后更改对象引用?【参考方案2】:

JVM 不可能每次都遍历整个堆 移动对象,并更新所有引用

我自己并不是 GC 方面的专家,但据我所知,它或多或少就是这样做的。参见例如这段文字:

相比之下,复制收集器将可达对象复制到另一个内存区域 当它们被遍历时。 [...] 经过这样 遍历所有幸存的对象驻留在的一个连续区域 内存,所有指针都已更新为指向新的对象位置。 [...] 在此过程中,GC 会构建一个对象图来跟踪“活动”对象,以便 它可以更新对其移动的任何对象的引用。

(http://wiki.osdev.org/Garbage_collection#Copy_collectors,强调我的)。

关于这种“永远占用”——复制(或移动)垃圾收集器背后的主要思想是实际上只有少量对象需要移动,因为大多数实例已经死了(即大多数实例非常短暂)。所以移动的对象数量很少,希望指向它们的引用数量也相当少。

无论如何,GC 必须建立一个对象引用列表(以找出哪些对象仍然被引用/活着并且需要复制),因此它可能可以重用该列表来更新引用。所以唯一的更新是“额外的工作”。

【讨论】:

+1 供参考,但不幸的是它不是 JVM 特定的。我将作为问题编辑发表评论... 实际上,GC 并没有像 OP 在他的问题中描述的那样遍历整个堆......【参考方案3】:

我不确定这是管理堆中对象引用的方式,但我怀疑 Java VM 分发给我们程序的对象引用不是实际的内存地址,而是指向的内部 JVM 引用JVM 中的实际地址(HashMap 或类似结构)。 IE。所有引用 objectA 的对象都将引用 [NOT address] 到 objectA,当 GC 发生时,JVM 不需要更新所有这些对象中的引用,只需更新它自己的 HashMap 中实际更改的地址。

【讨论】:

这是我的假设,但其他答案似乎不同意。不幸的是,到目前为止,这一切都只是猜测,因为没有人能指出一些解释这一点的 Sun/Oracle 链接。【参考方案4】:

JVM 不可能每次都遍历整个堆 移动对象,并更新所有引用;那肯定会 永远拿下

它确实会扫描整个堆以检测不再被任何人引用的对象并将它们标记为可以收集并将所有活动对象放置在紧凑的内存区域中以避免碎片。

它是如何工作的取决于所使用的垃圾收集算法,但这确实是一个耗时的过程,这就是为什么 Java(本身)不能在实时限制中使用的原因

【讨论】:

"scan through the whole heap" 只发生在 full GC 上,但是在 Minor GC 上对象的位置也会发生变化,这些对象可能会被老年代,不属于 Minor GC 的一部分。 垃圾回收算法有很多,甚至jdk在1.4和1.5或更高版本都不使用相同的。也许您应该研究您感兴趣的版本中使用的算法以获得您寻求的确切答案 我刚读完 2011 年 10 月出版的《Java 性能》(ISBN-10:0137142528),这是参考。不幸的是,这没有解释(或者我不知何故错过了它)。 您可能对java.sun.com/performance/reference/whitepapers/… 或oracle.com/technetwork/java/gc-tuning-5-138395.html 感兴趣。您应该专门搜索 Oracle JDK 中 GCC 的实现【参考方案5】:

通常,收集器不会走遍整个堆。它们识别活体并遍历它们。

例如,Hotspot 中的复制收集器,从根开始并识别所有活动对象。一旦识别出活动对象,它们就会被复制到堆上的新空间。在遍历所有活动对象时,它会对活动对象进行所需的地址修改。

一旦完成,留在旧空间中的都是死对象和已经移动的对象。该空闲空间由 GC 回收,并用于将来将其他活动对象移入其中。

所花费的时间与堆上活动对象的数量成正比。

【讨论】:

以上是关于如果 JVM 在进行 GC 时不断移动对象,它如何解析引用?的主要内容,如果未能解决你的问题,请参考以下文章

JVM的四种GC算法

JVM之GC算法解读

JVM GC算法

JVM 调优参数详解

JVM垃圾回收概念

你要的JVM垃圾回收器全在这了