【发布时间】:2020-09-29 17:27:59
【问题描述】:
标题几乎说明了一切 - 这些自我修复障碍是什么?为什么它们在 Shenandoah 2.0 中很重要?
【问题讨论】:
标签: java garbage-collection jvm shenandoah
标题几乎说明了一切 - 这些自我修复障碍是什么?为什么它们在 Shenandoah 2.0 中很重要?
【问题讨论】:
标签: java garbage-collection jvm shenandoah
这个解释将附在the first part 和the second part 我试图提出的一些答案Shenandoah 2.0。
要真正回答这个问题,我们需要了解load reference barrier 的实现方式以及GC cycle 的一般行为方式。
当某个GC cycle被触发时,首先选择垃圾最多的regions;即:集合集中的对象非常少(这在未来很重要)。
理解这个主题的最简单方法是通过一个例子。假设这是一个现在存在于某个区域的方案:
refA refB
|
---------
| mark |
---------
| i = 0 |
| j = 0 |
---------
该区域中存在一个对象,并且有两个引用指向它:refA 和 refB。 GC 启动并选择该区域进行垃圾收集。 同时应用程序中有活动线程尝试通过refA 和refB 访问此对象。由于此对象在某些时候是alive,因此需要将其撤离到一个新区域(mark-compact 阶段的一部分)。
所以:GC 是活动的,同时,我们通过refA/refB读取。当我们做这个阅读时,我们踩到了load-reference-barrier,实现了here。注意它内部有一些“过滤器”(通过一堆if/else 语句)。具体来说:
它检查“疏散当前是否正在进行”。这是通过在疏散首次开始时设置的线程本地标志来完成的。让我们假设答案是:是的。
它检查我们当前正在操作的对象是否在“集合集”中。这意味着它当前被标记为活动的。让我们假设这也是“是”。
最后的检查是确定这个对象是否已经“复制”到不同的区域(它被疏散了)。假设答案是“否”,即:obj == fwd。
此时,发生了一些事情。首先创建一个副本并mark becomes forwardee
refA refB
|
-------------- ---------
| forwardee | ---- | mark |
-------------- ---------
| i = 0 | | i = 0 |
| j = 0 | | j = 0 |
--------- ---------
只有在代码的后面,refA 和 refB 才会更新为指向 new(复制的)对象。但这意味着一件有趣的事情。这意味着直到refA 和refB 实际上指向新对象,它们当前指向的对象位于“集合集”中。所以,如果 GC 处于活动状态,即使 forwardee 已经建立,load-reference-barrier 仍然需要做一些工作。
所以Shenandoah 背后的非常 聪明人说:为什么不在forwardee 建立后立即更新那里的引用(或者当forwardee 已经为其他人所知时)参考)? And this is exactly what they did.
假设我们回到最初的绘图:
refA refB
|
---------
| mark |
---------
| i = 0 |
| j = 0 |
---------
我们再次“启用”所有过滤器:
有一个线程通过refA读取
GC 处于活动状态
refA 和 refB 后面的对象是活动的。
这就是“自我修复障碍”会发生的事情:
refB refA
| |
-------------- ---------
| forwardee | ---- | mark |
-------------- ---------
| i = 0 | | i = 0 |
| j = 0 | | j = 0 |
--------- ---------
区别很明显:refA 被当场通过CAS 移动到指向新对象。如果要通过refA 再次读取再次(GC 仍然处于活动状态),这将导致更快的加载引用屏障执行。为什么?因为refA 指向的对象不是在“集合集”中。
但这也意味着,如果我们通过refB 读取并看到fwd != obj - 代码可以执行相同的技巧并更新refB,当时第一次读取是通过refB 发生的。
据知情人士称,这提高了性能,我相信他们。
【讨论】: