G1并发标记过程观点不错的文章

原文地址 https://www.jianshu.com/p/aef0f4765098 

Marking Cycle Phase

算法的Marking cycle phase大概可以分成五个阶段:

  1. Initial marking phase:G1收集器扫描所有的根。该过程是和young GC的暂停过程一起的;
  2. Root region scanning phase:扫描Survivor Regions中指向老年代的被initial mark phase标记的引用及引用的对象,这一个过程是并发进行的。但是该过程要在下一个young GC开始之前结束;
  3. Concurrent marking phase:并发标记阶段,标记整个堆的存活对象。该过程可以被young GC所打断。并发阶段产生的新的引用(或者引用的更新)会被SATB的write barrier记录下来;
  4. Remark phase:也叫final marking phase。该阶段只需要扫描SATB(Snapshot At The Beginning)的buffer,处理在并发阶段产生的新的存活对象的引用。作为对比,CMS的remark需要扫描整个mod union table的标记为dirty的entry以及全部根;
  5. Cleanup phase:清理阶段。该阶段会计算每一个region里面存活的对象,并把完全没有存活对象的Region直接放到空闲列表中。在该阶段还会重置Remember Set。该阶段在计算Region中存活对象的时候,是STW(Stop-the-world)的,而在重置Remember Set的时候,却是可以并行的;

Initial marking phase

该阶段扫描所有的根,与CMS类似。所不同的是,该阶段是和young GC一起的。这里的young GC实际上是指的就是fully-young generational mode。

Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Guide的原文是"This phase is piggybacked on a normal (STW) young garbage collection"。

  有文章说本阶段只是定位出来要扫描的区域,这些区域中的根对象,但并未开始扫描

Root region scanning phase

该过程主要是扫描Survivor region中指向老年代的,在initial mark phase标记的引用及其引用的对象。这是一个很奇怪的步骤,因为在前面不论是Parallel Collector还是CMS,都没有这么一个步骤。

要理解这一点,要注意的是,算法的两种模式,不论是young GC还是mixed GC,都需要回收young region。因为实际上RS是不记录从young region出发的指针,例如,这部分指针包括young region - young region,也包括young-region - old region指针。那么就可能出现一种情况,一个老年代的存活对象,只被年轻代的对象引用。在一次young GC中,这些存活的年轻代的对象会被复制到Survivor Region,因此需要扫描这些Survivor region来查找这些指向老年代的对象的引用,作为并发标记阶段扫描老年代的根的一部分。

在理解了这一点的基础上,那么对于阶段必须在下一次young GC启动前完成的要求,也就理解了。因为如果第二次的young GC启动了,那么这个过程中,survivor region就可能发生变化。这个时候执行root region phase就会产生错误的结果。

Concurrent marking phase

在标记阶段,会使用到一个marking stack的东西。G1不断从marking stack中取出引用,递归扫描整个堆里的对象图,并且在bitmap上进行标记。这个递归过程采用的是深度遍历,会不断把对象的域入栈。

在并发标记阶段,因为应用还在运行,所以可能会有引用变更,包括现有引用指向别的对象,或者删除了一个引用,或者创建了一个新的对象等。G1采用的是使用SATB的并发标记算法。

在资料6中记录了使用SATB的两条原则:

  1. All accessible cells at the beginning of the garbage collection are eventually marked during the marked phase;
  2. Newly alocated cells during the garbage collection are never collected during the sweep phase of that garbage collection

在G1中,该算法的关键在于,如果在并发标记的时候,出现了引用修改(不包含新分配内存给对象),那么写屏障会把这些引用的原始值捕获下来,记录在log buffer中。而后再处理。后续的所有的标记,都是从原来的值出发,而不是从新的值出发的。

SATB是一个逻辑上存在概念,在实际中并没有任何真的实际的数据结构与之对应。叫这个名字,是因为,一旦进入了concurrent marking阶段,那么该在该阶段的运行过程中,即便应用修改了引用,但是因为SATB的写屏障记录下来了原始的值,在遍历整个堆查找存活对象的时候,使用的依然是原来的值。这就是在逻辑上保持了一个snapshot at the beginning of concurrent marking phase。

在处理新创建的对象,G1采用了不同的方式。G1用了两个TAMS变量了判断新创建的对象。一个叫做previous TAMS,一个叫做next TAMS。位于两者之间的对象就是新分配的对象。

并发标记阶段,bitmap和TAMS的作用如图:

  

该图的详细解释如下:

  1. A是第一次marking cycle的initial marking阶段。next bitmap尚未标记任何存活对象,而此时的previous TAMS被初始化为region内存地址起始值,next TAMS被初始化为top。top实际上就是一个region未分配区域和已分配区域的分界点;
  2. B是经过concurrent marking阶段之后,进入了remark阶段。此时存活对象的扫描已经完成了,因此next bitmap构造好了,刚好代表的是当下状态中region中的内存使用情况。注意的是,此时top已经不再与next TAMS重合了,top和next TAMS之间的就是在前面标记阶段之时,新分配的对象;
  3. C代表的是clean up阶段。C和B比起来,next bitmap变成了previous bitmap,而在bitmap中标记为垃圾(也就是白色区域的)的对应的region的区域也被染成了浅灰色。这并不是指垃圾对象已经被清扫了,仅仅是标记出来了。同时next TAMS和previous TAMS也交换了角色;
  4. D代表的是下一个marking cycle的initial marking阶段,该阶段和A类似,next TAMS重新被初始化为top的值;
  5. EF就是BC的重复;

Remark phase

该阶段是一个STW的阶段。引入该阶段的目的,是为了能够达到结束标记的目标。要结束标记的过程,要满足三个条件:

  1. concurrent marking已经追踪了所有的存活对象;
  2. marking stack是空的;
  3. 所有的log都被处理了;

前两个条件是很容易达到的,但是最后一个是很困难的。如果不引入一个STW的remark过程,那么应用会不断的更新引用,也就是说,会不断的产生log,因而永远也无法达成完成标记的条件。

Clean up

该阶段主要完成:

  1. 统计存活对象,这是利用RS和bitmap来完成的,统计的结果将会用来排序region,以用于下一次的CSet的选择;
  2. 重置RSet;
  3. 把空闲region放到空闲region列表中;

该阶段比较容易引起误解地方在于,Clean up并不会清理垃圾对象,也不会执行存活对象的拷贝。也就是说,在极端情况下,该阶段结束之后,空闲Region列表将毫无变化,JVM的内存使用情况也毫无变化。

Evacuation

Evacuation阶段STW的,大概可以分成两个步骤:第一个步骤是从Region中选出若干个Region进行回收,这些被选中的Region称为Collect Set(简称CSet);而第二个步骤则是把这些Region中存活的对象复制到空闲的Region中去,同时把这些已经被回收的Region放到空闲Region列表中。
这两个步骤又可以被分解成三个任务:

  1. 根据RS的日志更新RS:只有在处理完了RS的日志之后,RS才能够保证是准确的,完整的,这也是Evacuation是STW的重要原因;
  2. 扫描RS和其余的根来确定存活对象:该阶段实际上最主要依赖于RS;
  3. 拷贝存活对象:该阶段只要从2中确定的根触发,沿着引用链一直追溯下去,将存活对象复制到新的region就可以。这个过程中,可能有一部分的年轻代对象会被提升到老年代;

Evacuation的时机

Evacuation的触发时机在不同的模式下会有一些不同。在不同的模式下都相同的是,只要堆的使用率达到了某个阈值,就必然会触发Evacuation。这是为了确保在Evacuation的时候有足够的空闲Region来容纳存活对象。

在young GC的情况下,G1会选择N个region作为CSet,该CSet首先需要满足软实时的要求,而一旦已经有N个region已经被分配了,那么就会执行一次Evacuation。

G1会尽可能的执行mixed GC。唯一的限制就是mix GC也需要满足软实时的要求。

G1触发Evacuation的原则大概是:

  1. 如果被分配的young region数量满足young GC的要求,那么就会触发young GC;
  2. 如果被分配的young region数量不满足young GC,就会进一步考察加上old region的数量,能否满足old GC的要求;

为了理解这一点,可以举例来说,假如回收一个old region的时间是回收一个young region的两倍,也就是young region花费时间T,old region花费2T,在满足软实时目标的情况下,GC只能回收8T的region,那么:

  1. 假如应用现在只分配k(k<8)块young region,没有分配任何old region。这个时候又分配了一个old region,那么这个时候会立刻触发一次mixed GC,此次GC会选择k块young region和一块old region;
  2. 因此,在这种假设下,只要有可以回收的old region的时候,总是会先回收old region;
  3. 在没有任何old region的情况下,才有可能触发young region。

当然,在一般情况下,这些假设是不成立的。读者可以思考一下,在young GC和mixed GC达到软实时的要求下,young region和old region之间回收的花销不同会导致young GC和mixed GC会在什么情况下触发。

posted on   MaXianZhe  阅读(1147)  评论(1编辑  收藏  举报

编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示