重要免责声明:我不是 GC 专家/作家;下面写的所有内容都可能会发生变化,其中一些可能过于简单。请对此持保留态度。
我只会谈论Shenandoah
, as I think我明白;这不是分代 GC。
这里实际上有两个阶段:Mark
and Compact
。我在这里强烈强调,两者都是同时并且确实会在您的应用程序运行时发生(有一些非常短的 STW 事件)。
现在详细介绍。我已经解释了一些事情here https://stackoverflow.com/a/59120089/1059372,但因为这个答案与另一个不同的问题有关;我将在这里解释更多。我认为遍历活动对象的图对你来说不是什么新闻,毕竟你are读一本关于GC
。正如该答案所解释的,当应用程序完全停止(也称为进入安全点)时,识别活动对象很容易。没有人会改变你脚下的任何东西,地板是坚硬的,你可以控制一切。并行收集器就是这样做的。
真正痛苦的方法是并发做事情。 Shenandoah 采用了一种称为Snapshot at the beginning
(那本书解释了它 AFAIK),将称之为SATB
简而言之。基本上这个算法是这样实现的:“我将开始扫描同时对象图(来自 GC 根),如果有任何变化当我扫描时,我不会更改堆,但会记录这些更改并稍后处理”。
您需要质疑的第一部分是:当我扫描时。这是如何实现的?出色地,before做concurrent mark
,有一个STW event
called 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 知道并发标记何时发生。
但是并发标记是如何实现的呢?当堆本身发生变异(不稳定)时,如何扫描堆?你脚下的地板会增加更多的洞,也会消除它们。
这就是“Shenandoah魔法”。对堆的更改会被“拦截”,而不是直接持久化。因此,如果 GC 在此时执行并发标记,并且应用程序代码尝试更改堆,则这些更改将记录在每个线程中SATB queues
(开头的快照)。当并发标记结束时,这些队列将被清空(通过STW event
called Final Mark
)并再次分析那些被耗尽的更改(记住在STW event
now).
当这个阶段最终分数结束了 GC 知道什么是活着的,因此什么是隐式垃圾.
接下来是紧凑阶段。Shenandoah
现在应该将活动对象移动到不同的区域(以紧凑的方式)并将当前区域标记为我们可以再次分配的区域。当然,在简单的STW phase
,这很容易:移动对象,更新指向它的引用。完毕。当你必须这样做的时候同时...
您不能将对象简单地移动到不同的区域并then一一更新您的参考资料。想一想,假设这是我们的第一个状态:
refA, refB
|
---------
| i = 0 |
| j = 0 |
---------
该实例有两个引用:refA
and refB
。我们创建该对象的副本:
refA, refB
|
--------- ---------
| i = 0 | | i = 0 |
| j = 0 | | j = 0 |
--------- ---------
我们创建了一个copy,但尚未更新任何参考资料。我们现在移动单个引用以指向副本:
refA refB
| |
--------- ---------
| i = 0 | | i = 0 |
| j = 0 | | j = 0 |
--------- ---------
现在有趣的部分是:ThreadA
does refA.i = 5
, while ThreadB
does refB.j = 6
所以你的状态变成:
refA refB
| |
--------- ---------
| i = 5 | | i = 0 |
| j = 0 | | j = 6 |
--------- ---------
你怎么merge现在这些物体?老实说 - 我不知道这是否可行,而且这也不是一条可行的路线Shenandoah
took.
相反,解决方案来自Shenandoah
恕我直言,做了一件非常有趣的事情。一个额外的指针添加到每个实例,也称为转发指针:
refA, refB
|
fwdPointer1
|
---------
| i = 0 |
| j = 0 |
---------
refA
and refB
指着fwdPointer1
, while fwdPointer1
到真实的对象。现在让我们创建副本:
refA, refB
|
fwdPointer1 fwdPointer2
| |
--------- ---------
| i = 0 | | i = 0 |
| j = 0 | | j = 0 |
--------- ---------
现在,我们要切换所有引用(refA
and refB
) 指向副本。如果你仔细观察,这只需要改变一个指针 -fwdPointer1
. Make fwdPointer1
指向fwdPointer2
你就完成了。这意味着一次更改而不是two(在此设置中)refA
and refB
。这里更大的好处是您不需要扫描堆并找出指向您的实例的引用。
有没有办法自动更新引用?当然 :AtomicReference
(至少在java中)。这里的想法几乎是一样的,我们原子地改变了fwdPointer1
via a CAS
(比较和交换),例如:
refA, refB
|
fwdPointer1 ---- fwdPointer2
|
--------- ---------
| i = 0 | | i = 0 |
| j = 0 | | j = 0 |
--------- ---------
So, refA
and refB
指向fwdPointer1
,它现在指向我们创建的副本。通过单CAS
操作,我们已经切换了同时对新创建的副本的所有引用。
然后,GC 可以简单地(同时) 更新所有参考文献refA
and refB
指向fwdPointer2
。最后有这个:
refA, refB
|
fwdPointer1 ---- fwdPointer2
|
--------- ---------
| i = 0 | | i = 0 |
| j = 0 | | j = 0 |
--------- ---------
因此,左侧的对象现在是垃圾:没有引用指向它。
但是,我们需要了解弊端,天下没有免费的午餐。
-
首先,很明显:Shenandoah
添加一个机器头堆中的每个实例(进一步阅读,因为这是错误的;但使理解更容易)。
-
这些副本中的每一个都会在新区域中生成一个额外的对象,因此在某个时刻,同一对象将至少有两个副本(需要额外的空间)Shenandoah
发挥作用,本身)。
-
When ThreadA
does refA.i = 5
(来自前面的示例),它如何知道是否应该尝试创建副本、写入该副本并CAS
that forwarding pointer
vs 简单地写入对象?请记住,这是同时发生的。与 相同的解决方案concurrentMarkingActive
旗帜。有一面旗帜isEvacuationToADifferentRegionActive
(不是真实姓名)。如果该标志是true
=> Shenandoah Magic,否则只需按原样写入即可。
如果您真正理解最后一点,您自然的问题应该是:
“等一下!这是否意味着谢南多厄做了一个if/else
反对isEvacuationToADifferentRegionActive
对于每个和单个写入实例 - 是原语还是引用?这是否也意味着每次读取都必须通过forwarding pointer
?"
答案以前是 YES;但事情发生了变化:通过这个问题 https://bugs.openjdk.java.net/browse/JDK-8221766(尽管我让它听起来比实际情况要糟糕得多)。现在他们使用Load
整个对象的障碍,更多细节here https://developers.redhat.com/blog/2019/06/27/shenandoah-gc-in-jdk-13-part-1-load-reference-barriers/。而不是在每次写入时设置屏障(即if/else
反对标志)并通过取消引用forwarding pointer
每次阅读,他们都会移动到load barrier
。基本上就是这样做的if/else
仅当您加载对象时。由于写入它意味着首先读取,因此它们保留了“空间不变性”。显然这更简单、更好、更容易优化。万岁!
请记住forwarding pointer
?好吧,它已经不存在了。我还不明白它的全部细节,但它必须做一些有可能使用的事情mark word
和from space
由于添加了负载屏障,因此不再使用。很多更多详细信息请点击这里 https://developers.redhat.com/blog/2019/06/28/shenandoah-gc-in-jdk-13-part-2-eliminating-the-forward-pointer-word/。一旦我了解了它的内部运作原理,我就会更新这篇文章。
G1
与什么没有太大不同Shenandoah
是的,但问题在于细节。例如Compact
在...阶段G1
is a STW
事件,总是。G1
is always世代相传——无论你是否愿意(Shenandoah
can有点像那样 - 有一个设置可以控制它),等等。