闲谈JVM(八):深入理解JVM GC Roots

前言

闲谈JVM(七):浅谈JVM GC之CMS GC

深入理解JVM虚拟机:(二)垃圾收集器概述

在上一篇中,我们对CMS收集器的工作原理进行了详细分析,在分析CMS执行垃圾回收的过程中,提及到了GC Roots的概念,本篇,我们对GC Roots进行展开,详细分析一下JVM中的GC Roots。

GC Roots

JVM在进行垃圾回收时,如何判断一个对象是否可以被回收,是通过可达性分析来判定对象是否存活的

垃圾回收示意

如上图所示,JVM通过GC Roots来进行判断一个对象是否可以被进行回收。

所谓GC Roots,或者说Tracing GC的“根集合”,就是一组必须活跃的引用。

例如说,这些引用可能包括:

  • 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
  • JVM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot JVM里的Universe里有很多这样的引用。
  • JNI handles,包括global handles和local handles。
  • (看情况)所有当前被加载的Java类。
  • (看情况)Java类的引用类型静态变量。
  • (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)。
  • (看情况)String常量池(StringTable)里的引用。

注意,是一组必须活跃的引用,不是对象。

执行GC操作的根本思路就是:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,其余对象(也就是没有被遍历到的)就自然被判定为死亡。

注意再注意:是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。
GC Roots这组引用是Tracing GC的起点。要实现语义正确的Tracing GC,就必须要能完整枚举出所有的GC Roots,否则就可能会漏扫描应该存活的对象,导致GC错误回收了这些被漏扫的活对象。

Card Table

上面介绍了可能作为GC Roots的引用类型,而JVM在进行GC时,同时要处理一种情况,即跨生代引用的情况,在执行部分生代收集时,从GC堆的非收集部分指向收集部分的引用,也必须作为GC roots的一部分。

例如,当进行新生代的GC时,其中某些新生代的对象引用指向了老生代的对象,那么这种引用,也需要作为GC Roots的一部分。

跨生代引用

那么GC收集器是如何知道所在的进行回收的生代中,哪些对象存在了跨生代引用呢?

最简单的实现方式,就是将全部堆区中的对象扫描一遍,以此来确定相互直接的引用关系,但是显而易见,这样的做法是不可行的,会带来巨大的性能损耗。

为此,JVM使用了一个叫做Card Table(卡表),也可以称作Remembered Set(记忆结果集)的数据结构,用来标记老生代的某一块内存区域中的对象是否持有新生代对象的引用。

Remembered Set是一种抽象概念,而Card Table可以是remembered set的一种实现方式。

Remembered Set是在实现部分垃圾收集(partial GC)时用于记录从非收集部分指向收集部分的指针的集合的抽象数据结构。

Card Table

Card Table的数量取决于老年代的大小和每张Card对应的内存大小,每张Card在Card Table中对应一个bit位,当老生代中的某个对象持有了新生代对象的引用时,JVM就把这个对象对应的Card所在的Card Table中位置标记为dirty(bit位设置为1),这样在进行新生代GC时就不用扫描整个老生代,而是扫描Card Table中Card为Dirty对应的那些内存区域,如果这个Card没有对新生代的引用了,那么新生代GC就会把它标记为clean。

Card Table结构

而每当生代中的对象引用关系发生变化时,JVM需要知道这个变化,并更新Card Table,这个操作称之为write barriers(写屏障),每当对象的引用关系发生了变化时,write barrier会拦截所有新插入的引用关系,并且按需要记录新的引用关系,更新Card Table,以此保证Card Table的准确性。

在此引用Azul对Remembered Set的解读,仅供参考:

Generational collectors use a ‘remembered set’ to track all references into the young generation from the outside, so the collector doesn’t have to scan for them.

This set is also used as part of the ‘roots’ for the garbage collector. A common technique is ‘card marking’, which uses a bit (or byte) indicating that a word or region in the old generation is suspect.

These ‘marks’ can be precise or imprecise, meaning it may record the exact location or just a region in memory.

Write barriers are used to track references from the young generation into the old generation and keep the remembered set up-to-date.

Oracle’s HotSpot uses what’s called a ‘blind store’. Every time you store a reference it marks a card.

This works well, because checking the reference takes more CPU time, so the system saves time by just marking the card.

CMS的实现

上面我们了解了JVM中关于GC过程中Card Table的作用,事实上,对于不同的GC收集器,跨生代引用记录的Card Table的实现有所不同,这里我们对CMS GC的Card Table进行展开分析。

CMS GC是JVM GC收集器中唯一可以只对老生代进行单独收集的GC收集器,CMS GC对于Card Table的实现较为简单,它只维护了一个Card Table,来记录老生代对象对于新生代对象的引用,当新生代执行GC操作时,查找老生代的GC Roots时,只需要扫描Card Table,即可找到老生代GC Roots。

而当CMS GC执行老生代GC操作时,由于没有维护新生代对象指向老生代对象引用的Card Table,因此会将整个新生代作为GC Roots进行扫描。

CMS并发标记带来的问题

在CMS的并发标记阶段,可能会出现两个并发问题:

第一,CMS在执行并发标记,同时,应用在修改老年代中对象的引用。这时候,老年代的引用状态会发生改变,所以CMS要想办法把这种改变记录下来。

CMS使用Card Table来记录这些改变,把发生改变的对象所在的Card标记为dirty,然后在最终标记阶段再次扫描这些标记为dirty的Card。(不过这样也会产生浮动垃圾)

第二,CMS在执行并发标记,同时,新生代GC开始运行。注意,新生代GC也需要扫描Card Table,在扫描的时候,要对标记为dirty的Card进行分析,如果这个Card没有对新生代的引用了,那么新生代GC就会把它标记为clean,但是这样导致CMS收集器在最终标记阶段无法扫描这个Card。

那这样到底有影响吗?想象一种可能的情况,就是在CMS在执行并发标记的时候,其他线程先改变了一个Card里面的对象引用,然后新生代GC开始运行(这时CMS仍然在执行并发标记),新生代GC如果扫描到这个dirty Card不再有新生代的引用,那么就把它标记为clean。这个时候就会出现了标记遗漏的情况了。

为了解决上述的问题,Mod Union Table被引入了,它是一个位向量,每个单元的大小只有1位,每个单元对应一个Card(Card的大小是512字节,Card Table每一个单元的大小是1个字节),在新生代GC处理dirty Card之前,先把该Card在Mod Union Table里面的对应项置位。

这样,CMS在执行最终标记阶段的时候,就会扫描Mod Union Table和Card Table里面被标记的项,以此保证标记的准确性。

结语

本篇我们对JVM GC操作中非常关键的一个概念GC Roots进行了详细展开,GC Roots是作为GC操作的起点,理解GC Roots可以更好的帮助我们更好的理解GC的过程,关于Card Table部分的概念较为抽象,如果您对本篇中提到的观点有所疑问,欢迎一起讨论。

引用:

How actually card table and writer barrier works?
关于incremental update与SATB的一点理解
JVM调优:CardTable简介

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值