为什么并发 GC 需要 remark 阶段

Why remark phase is needed on concurrent GC

并发 GC 需要 remark phaseremark phase的作用是在concurrent mark phase期间标记修改的对象。但是我觉得如果我们只在concurrent mark phase期间标记新创建的对象,就不需要执行remark phase

remark phase 因为修改了对象所以需要。修改可以是两种类型。一个是创建新对象,另一个是修改指向另一个对象的指针。如果我们标记新创建的对象,就可以轻松解决新对象问题。而修改指向另一个对象的指针实际上不是问题。因为

Dead object can not revive

死对象意味着没有人可以指向该对象。他们如何复活?所以修改后的指针应该指向已经标记好的对象。这意味着不需要执行 remark.

有人会这么说,"Marking new object on its creation is too expensive. So they cannot be marked during concurrent mark phase and that's the reason why remark phase is needed"。这似乎是合理的。但这会引起另一个问题。 remark 怎么可能不从根开始遍历每一个对象呢?如果 remark phase 应该从根开始遍历所有对象,那么 concurrent mark phase 所做的工作是无用的。或者如果remark phase只遍历修改过的对象,修改了哪个对象的信息应该保存在某处。我认为这可能比标记 .

贵得多

我错了吗?应该是错的。但是我不知道哪一点是错的。

And modified pointer to another object is not a problem in fact. Because

Dead object can not revive

他们真的不能,但你知道哪些 object 已经死了吗?不!为什么?

初始标记阶段 之后你并不知道它,因为你只看线程堆栈而不关注引用。

你不知道在并发标记阶段之后是否会出现如下情况:

  • 线程读取字段 a.x 并将其值存储在其寄存器中(或其堆栈或其他地方)。
  • 然后这个线程设置a.x = null(或其他)。
  • GC 过来看到 null 那里。
  • 然后线程将 a.x 恢复为其先前的值。

现在,GC 错过了指向 object a.x 的点。虽然上述场景不是很常见,但它可能会发生,并且存在更现实(和更复杂)的场景。

所以有必要再看看修改后的内存,也就是remark阶段。幸运的是,不必再次扫描整个内存,因为使用了 card table


恐怕this (otherwise nice) explanation在这一点上有点误导:

The remark phase is a stop-the-world. CMS cannot correctly determine which objects are alive (mark them live), if the application is running concurrently and keeps changing what is live.

线程确实改变了什么是实时的,但它们也改变了你可以看到的实时。这就是问题所在。

This article说的很清楚:

Part of the work in the remark phase involves rescanning objects that have been changed by an application thread (i.e., looking at the object A to see if A has been changed by the application thread so that A now references another object B and B was not previously marked as live).

我会说:当你搜索一个房间又一个房间时,children 移动它们可能会遗漏你的眼镜。

关于场景的注释

我很确定,上述情况是可能的,只是程序通常不会这样做。对于一个非常现实的例子,请考虑

void swap(Object[] a, int i, int j) {
    Object tmp = a[i];
    a[i] = a[j];
    // Now the original reference a[i] is in a register only.
    a[j] = tmp;
}

GC 必须始终查看自当前周期开始以来已修改的每个存储引用,因为 GC 尚未查看的某个位置的引用可能会存储在它查看过的位置并从其原始位置移除。并发 GC 可以在不停止世界的情况下重新访问修改后的引用,但在这样做的同时,其他线程可能会继续修改更多引用。

如果 10% 的对象在 GC 进行全面扫描时被修改,那么在不停止世界的情况下访问这 10% 的对象可能是值得的,但是当它访问这 10% 时,其他线程可能会干扰 3% .并发访问那 3% 可能是值得的,但其他线程可能会干扰 2%。进一步的传递可能会减少在 stop-the-world 模式下需要完成的工作量,但是当其他线程仍在干扰引用的过程中时,GC 不太可能完全完成。除非所有线程自发地停止对引用执行任何操作,否则 GC 将永远无法在不停止世界的情况下 100% 完成。

请注意,对于 GC 设计来说,可能会承诺永远不会停止世界超过一定的时间,代价是可能会导致新的分配请求挨饿。 GC 无法确定完成一个循环需要多长时间,因为在循环完成之前,它无法知道是否存在对 as 的大型集合的根的尚未发现的引用-尚未发现的物体。另一方面,如果 GC 达到了一个点——在没有此类发现的情况下——它将 期望 在 1 毫秒内完成,但随后让世界如果新发现会导致它花费超过 2 毫秒的时间,则重新启动。让世界重新启动将需要在 GC 释放任何内存之前再次停止世界,并且可能很难保证没有任何事件组合会导致无休止的中止循环 "final cleanup" 尝试,但是如果允许的 "stop the world time" 相对于代码对引用所做的 "churning" 的数量是合理的,那么失败的最终清理应该是罕见的,并且重复的异常如此。