在阅读The Garbage Collection Handbook上的Mark-Compact一章时,提出了一系列替代方案,但是大多数替代方案看起来都是过时的/理论性的(例如2指压实和Lisp2 3遍方法需要每个对象额外的标题字)。
是否有人知道HotSpot在运行Mark-Compact时会使用哪种算法(我认为是老一代)?
谢谢
答案 0 :(得分:1)
免责声明:我不是GC专家/作家;所有在下面写的东西都可能会发生变化,其中有些可能过于简单。请把它和一粒盐一起吃。
按照我的理解,我只会谈论Shenandoah
;这不是世代的GC。
这里实际上有两个阶段:Mark
和Compact
。在这里,我将特别强调两者都是 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 = 5
做ThreadB
,所以您的状态变为:
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
,您就完成了。这意味着一个单一的更改,而不是fwdPointer2
和refA
的两个(在此设置中)。这样做的最大好处是,您无需扫描堆并找出指向您的实例的引用。
有没有办法自动更新引用?当然: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
进行true
到if/else
的写入-是原始的还是引用的?这还意味着每次读取必须是是否通过isEvacuationToADifferentRegionActive
访问?”
答案曾经是 是;但情况发生了变化:via this issue(尽管我听起来比实际情况差很多)。现在他们对整个对象使用forwarding pointer
障碍,更多详细信息here。他们在每次写入时都没有障碍(Load
相对于标志)和通过if/else
进行的每次读取取消引用,而是移至forwarding pointer
。基本上,仅在加载对象时执行load barrier
。由于对其进行写意味着首先阅读,因此它们保留了“空间不变”。显然,这更简单,更好,更容易优化。哇!
还记得if/else
吗?好吧,它不再存在。我还不了解它的全部细节(但是),但是它必须做一些事情来使用forwarding pointer
和mark word
,因为增加了负载屏障,用了。很多more details here。一旦我了解了它在内部的真正工作原理,便会更新该帖子。
from space
与G1
并没有太大区别,但细节之处在于魔鬼。例如,Shenandoah
中的Compact
阶段总是G1
事件。 STW
总是 代-即使您不愿这样做(G1
都可以像这样-有一种设置可以控制)等