HotSpot使用的Mark-Compact算法是什么?

时间:2019-11-26 05:57:13

标签: garbage-collection jvm jvm-hotspot mark-compact

在阅读The Garbage Collection Handbook上的Mark-Compact一章时,提出了一系列替代方案,但是大多数替代方案看起来都是过时的/理论性的(例如2指压实和Lisp2 3遍方法需要每个对象额外的标题字)。

是否有人知道HotSpot在运行Mark-Compact时会使用哪种算法(我认为是老一代)?

谢谢

1 个答案:

答案 0 :(得分:1)

免责声明:我不是GC专家/作家;所有在下面写的东西都可能会发生变化,其中有些可能过于简单。请把它和一粒盐一起吃。

按照我的理解,我只会谈论Shenandoah;这不是世代的GC。

这里实际上有两个阶段:MarkCompact。在这里,我将特别强调两者都是 concurrent ,并且确实会在您的应用程序运行时发生(带有一些非常短的STW事件)。

现在到细节。我已经解释了here的一些问题,但是因为该答案与某种不同的问题有关;我将在这里解释更多。我假设遍历活动对象图对您来说不是什么新闻,毕竟您正在读GC的书。正如该答案所解释的,当应用程序完全停止(也称为安全点)时,识别活动对象很容易。没有人在脚下改变任何东西,地板坚硬,您可以控制一切。并行收集器可以做到这一点。

真正痛苦的方式是并发做事。 Shenandoah使用一种称为Snapshot at the beginning的算法(该书将其解释为AFAIK),简称为SATB。基本上,该算法的实现方式如下:“我将开始同时扫描 对象图(从GC根开始),如果在扫描过程中有什么变化,我不会改变堆,但将记录这些更改并在以后处理。”

您需要询问的第一部分是:我扫描时。如何实现的?好吧,执行concurrent mark之前,有一个STW event叫做Initial Mark。在该阶段要完成的事情之一是设置一个标志,指示并发标记已开始。稍后,在执行代码时,将检查该标志(Shenandoah将采用解释器中的更改)。用伪代码:

if(!concurrentMarkingActive) {
    // do whatever you were doing and alter the heap
} else {
    // shenandoah magic
}

在可能看起来像这样的机器代码中:

test %r11, %r11 (test concurrentMarkingActive flag)
jne // concurrent marking is currently active

现在,GC知道何时进行并发标记。

但是如何同时执行标记。当堆本身发生突变(不稳定)时,如何扫描堆?脚下的地板增加了更多的孔,并且也将其移除。

那是“神仙多亚魔法”。对堆的更改被“拦截”,而不是直接保留。因此,如果GC在此时进行并发标记,并且应用程序代码尝试使堆发生变化,则这些更改将记录在每个线程SATB queues中(开头是快照)。当并发标记结束时,将耗尽这些队列(通过称为STW event的{​​{1}}),并再次分析那些耗尽的更改(现在记在Final Mark下)。

当此阶段最终标记结束时,GC知道什么仍然存在,因此隐含了什么垃圾


紧致阶段是下一个。现在STW event应该将活动对象移动到不同的区域(以紧凑的方式),并将当前区域标记为可以再次分配的区域。当然,在简单的Shenandoah中,这很容易:移动对象,更新指向该对象的引用。做完了当您必须同时进行 ...

您不能拿走对象并将其移动到另一个区域,然后然后一个接一个地更新您的引用。考虑一下,让我们假设这是我们的第一个状态:

STW phase

对此实例有两个引用: refA, refB | --------- | i = 0 | | j = 0 | --------- refA。我们创建该对象的副本:

refB

我们创建了副本,但尚未更新任何参考。现在,我们将单个引用指向副本:

refA, refB
     |
 ---------       ---------
 | i = 0 |       | i = 0 |
 | j = 0 |       | j = 0 |
 ---------       ---------

现在有趣的部分: refA refB | | --------- --------- | i = 0 | | i = 0 | | j = 0 | | j = 0 | --------- --------- ThreadA,而refA.i = 5ThreadB,所以您的状态变为:

refB.j = 6

您现在如何合并这些对象?老实说-我什至不知道这是否有可能,而且 refA refB | | --------- --------- | i = 5 | | i = 0 | | j = 0 | | j = 6 | --------- --------- 也不走这条路线。

相反,Shenandoah的解决方案做的非常有趣,恕我直言。向每个实例添加的额外指针,也称为转发指针

Shenandoah

refA, refB | fwdPointer1 | --------- | i = 0 | | j = 0 | --------- refA指向refB,而fwdPointer1指向实际对象。现在创建副本:

fwdPointer1

现在,我们要切换所有引用( refA, refB | fwdPointer1 fwdPointer2 | | --------- --------- | i = 0 | | i = 0 | | j = 0 | | j = 0 | --------- --------- refA)以指向副本。如果仔细观察,这只需要更改单个指针-refB。使fwdPointer1指向fwdPointer1,您就完成了。这意味着一个单一的更改,而不是fwdPointer2refA两个(在此设置中)。这样做的最大好处是,您无需扫描堆并找出指向您的实例的引用。

有没有办法自动更新引用?当然:refB(至少在Java中)。这里的想法几乎是相同的,我们通过AtomicReference通过原子方式更改fwdPointer1(比较和交换),如下所示:

CAS

因此, refA, refB | fwdPointer1 ---- fwdPointer2 | --------- --------- | i = 0 | | i = 0 | | j = 0 | | j = 0 | --------- --------- refA指向refB,它现在指向我们创建的副本。通过一次fwdPointer1操作,我们已同时 切换了所有对新创建副本的引用。

但是,我们需要了解缺点,没有免费的午餐。

  • 首先,很明显:CAS添加了一个计算机标头,该标头堆中的每个实例(进一步阅读,因为这是错误的;但是使理解更容易)。

  • 这些副本中的每一个都会在新区域中生成一个额外的对象,因此在某个点上至少会有两个相同对象的副本(Shenandoah起作用所需的额外空间,因此)。

  • Shenandoah执行ThreadA(来自上一个示例)时,如何知道是否应尝试创建副本,写入该副本,然后refA.i = 5将{ {1}}与仅对对象进行写入?请记住,这是同时发生的。与CAS标志相同的解决方案。有一个标记forwarding pointer(不是实际名称)。如果该标志是concurrentMarkingActive =>雪兰多亚魔术,否则只需照原样写。

如果您真的了解了最后一点,您的自然问题应该是:

  

“等待第二个!这是否意味着对于每个实例,Shenandoah对isEvacuationToADifferentRegionActive进行trueif/else的写入-是原始的还是引用的?这还意味着每次读取必须是是否通过isEvacuationToADifferentRegionActive访问?”

答案曾经是 ;但情况发生了变化:via this issue(尽管我听起来比实际情况差很多)。现在他们对整个对象使用forwarding pointer障碍,更多详细信息here。他们在每次写入时都没有障碍(Load相对于标志)和通过if/else进行的每次读取取消引用,而是移至forwarding pointer。基本上,仅在加载对象时执行load barrier。由于对其进行写意味着首先阅读,因此它们保留了“空间不变”。显然,这更简单,更好,更容易优化。哇!

还记得if/else吗?好吧,它不再存在。我还不了解它的全部细节(但是),但是它必须做一些事情来使用forwarding pointermark word,因为增加了负载屏障,用了。很多more details here。一旦我了解了它在内部的真正工作原理,便会更新该帖子。

from spaceG1并没有太大区别,但细节之处在于魔鬼。例如,Shenandoah中的Compact阶段总是G1事件。 STW总是 代-即使您不愿这样做(G1 都可以像这样-有一种设置可以控制)等