zxdsb666. 发表于 2021-8-13 10:44

深入理解JVM - Shenadoah

# 深入理解JVM - Shenadoah

# 前言
​        zgc和shenadoah的收集器是面向未来的收集器,目前还处于不断完善的阶段,虽然我们平时可能不太用的上,但是了解和基本掌握它是必须的,关于这一块网上的内容确实比较少,所以个人还是使用了书本里面的内容进行总结。

​        另外这两个垃圾收集器是完全舍弃分代这个概念的,注意是完全舍弃,并不是类似G1收集器虽然使用了分区但是本质上还是分代收集的收集器。

​        由于这两个收集器的内容较多,这里分开进行讲解,本篇讲解Shenadoah收集器。



# 思维导图:

不想看文字的,可以查看思维导图:https://www.mubucm.com/doc/7L4W-FA0CFB

![](C:\Users\Administrator\Desktop\20210813103622.png)



# 概述

# 低延迟垃圾收集器

​        在正式介绍之前,有必要说明一下整个背景,现代的垃圾收集器考虑的点主要为下面这三个条件:**内存占用,吞吐量,延迟**,通过之前的收集器介绍,我们知道了虽然主流的g1收集器在标记阶段实现了并发,但是在初始标记和筛选回收阶段还是需要进行阶段性的stop world的,这个垃圾收集器并没有做到真正意义上的并发,并且由于分区+region分代的设计限制,必然会产生垃圾收集的停顿。所以未来的垃圾收集器主要目标将会是面向极低延迟进军,也就是努力实现用户线程和垃圾收集器线程的完全并发运行。

​        值得一提的是虽然新生的低延迟垃圾收集器抛弃了分代的概念,但是G1的Region分块以及垃圾停顿模型保留了下来,我们也可以看到几乎所有的垃圾收集器都是基于前人的努力成果进行改进,所以不需要十分恐惧内容很难或者是完全颠覆想法。



## g1和cms实现并发的细节
​        之前的文章提到了**增量更新**和**原始快照**,cms使用的是增量更新,g1使用的是原始快照,另外cms使用标记-清除的算法,免不了内存碎片,而g1虽然使用标记-整理,但是终究还是需要进行暂停的,所以这是一个非常棘手的问题。


​   
# Shenadoah
## 简介
​        这款收集器是首款非jdk官方开发的垃圾收集器,由redhat公司开发,后续被捐赠给eclipse基金会,目前由eclipse基金会进行维护和管理。虽然Shenadoah从设计的细节来看有很多需要完善的地方,但是确实已经具备了独立作为垃圾收集器使用的条件。

​        比较可惜的是oracle因为商业竞争的问题会把shenandoah通过条件编译的手段进行排除使用比较麻烦,所以shenadoah只能存在于**openJDK**无法在OracleJdk上进行部署,但是这款垃圾收集器依然值得我们学习。



## 特点
​        下面来说一下shenadoah的特点:

**Region**

​        和G1收集器的设计原理一样使用的是region进行分块,同样有着大对象的概念,默认的策略也是根据算法回收最有价值的region。

**没有分代和连接矩阵**

​        注意是没有分代的概念,默认不使用分代收集,换言之就是没有新生代和老年代的说法。那要怎么设计?Shenandoah的解决方案是使用独立构建的“连接矩阵”全局数据结构来维护region的引用关系,也不要被连接矩阵这种名词给吓到了,其实本质上是一个**二维数组**结构,比如我们在**Region N**引用了**Region M**,那么就会在对应的N行M列上打上一个标记,也就是说全局的对象引用都会通过这个表来维护,这也意味着连接矩阵会随着对象的增长不断膨胀。

> G1收集器是放弃固定分代而是使用分区的设计,然而分区本质上还是分代的,只不过可以自由决定属于哪一个分代。

​        下面直接从书里面拷了一张图来显示连接矩阵的设计:

![](https://gitee.com/lazyTimes/imageReposity/raw/master/img/20210813095845.png)

> 不得不说的是这个连接矩阵在设计上是仁者见仁智者见智了,维护一个矩阵虽然很方便但是随着对象的增多会呈现出指数性的表膨胀,这样来看还是一个值得商榷的设计,这一点在后续的垃圾收集器zgc介绍中会提到,zgc发现了连接矩阵的问题,采用了一些改进手段来解决表膨胀的问题。

**支持并发收集和整理**

​        支持并发收集和整理,可以实现标记和整理阶段完全和用户线程并发执行。




## 算法细节
​        那么这个收集器是如何做到这些事情的呢,在介绍工作流程之前,我们来聊一下算法的实现细节。


### brooks pointer
​        历史原因不过多介绍,这里说明一下这个值的含义:**转发指针**。转发指针是什么呢?它是用来解决对象移动和用户程序并发的一种解决方案。

​        Brooks pointer的工作原理:就是在对象的结构布局上增加一个新的引用字段,这个引用**通常情况下指向自己**,当对象发生转移的时候,brooks pointer会指向**新引用的地址**,这样指向旧引用的对象就可以修复引用指向新对象,这种结构在形式上和JVM的句柄定位类似,都是使用一种间接的访问形式,差别是转发指针会分散存在对象头内部。(之前我们讨论过对象头是动态扩展的格式)

​        这种设计形式也有点类似于链表的设计形式。

![](https://gitee.com/lazyTimes/imageReposity/raw/master/img/20210813100222.png)

> 补充:**之前如何解决对象引用问题?**
>
> ​        使用的是一种在原有的对象内存之上**设置保护陷阱+异常处理**的方式,一旦出现访问旧对象的行为,就会进入到保护陷阱当中,并且进入异常处理器进行代码逻辑和引用的修复。这种方式看起来十分的有效,但是如果没有操作系统的支持,就需要通过不断的**用户态到内核态的切换**,需要耗费更多的上下文切换资源,也是一种非常耗费性能的妥协办法。

**缺点**:

​        虽然转发指针被优化到只有一行汇编指令的程度,但是依然要消耗对象访问的效率,当然这个方案毫无疑问是比内存陷阱要好,

**并发问题**:

​        转发指针的设计意味着他必然有并发的问题,如果发生并发操作,就需要保证写操作必须是在新复制的对象下面,不妨考虑下面的问题:

1. 收集器线程复制了新的对象副本
2. 用户线程更新对象的某个字段
3. 收集器线程更新转发指针的引用值为新副本地址。

​        如果不防范这三个问题,就会导致用户线程的对象变更都是操作旧对象,所以必须针对指针的访问操作采取同步的措施。解决办法和对象的引用分配方式也是类似的也是使用**CAS+更新失败重试的操作机制**。

​        最后,还需要注意的是Shenadoah必须使用读写屏障去维护brooks pointer(并发问题决定了要时刻保持同步),这个代价是非常大的。下面我们接着来讲讲读写屏障的问题。



### 读写屏障

​        shenandoah不仅使用了写屏障还使用了读屏障,读屏障也是类似对象引用操作的一个AOP的切面,我们都知道对象的读操作肯定是要多于写操作的,所以使用读屏障的代价要大很多。

> 写屏障的概念可以看专栏之前的文章:[深入理解JVM - Hotspot算法细节#写屏障](https://juejin.cn/post/6994642246842712100#heading-18)

​        当然Shenandoah开发者也意识到这个问题,在**JDK13的**版本中,改用了基于“引用访问屏障”的方式解决读屏障的问题,“引用访问屏障”指的是只拦截对象类似是引用类型的数据进行访问屏障的拦截,这样就可以省去一些原生类型并发修改访问的操作,减少庞大的读屏障维护开销。
> 从这里也可以看出来Redhat的开发团队在设计jvm垃圾收集器上的经验缺乏,但是可以及时调整解决问题。



## 工作过程:

​        shenandoah的工作步骤可以划分为9个步骤,最新版本的shenandoah还在初始标记的步骤前面增加了三个步骤,简单理解为分代收集当中的Minor GC操作即可。

接下来说一下具体的步骤:

1. 初始标记:和G1一样,首先标记出所有的GC ROOT关联的对象,**注意这个阶段是需要停顿的**。
2. **并发标记**:和G1一样,根据GC ROOT遍历对象图,标记出所有的可达对象,这个阶段**和用户线程一起并发**。
3. 最终标记:还是和G1一样,处理剩下的对象扫描操作,同时计算出回收价值最高的Region,最终标记阶段有**一小段的暂停**。
4. 并发清理:这个阶段用于清理整个区域一个存活对象都没有的Region,这个阶段是**并发执行**的。
5. **并发回收**:**核心差异点,**在这个阶段,会把回收集里面存活对象先复制一份到到其他未使用的Region,但是要注意这个操作并不是同步的而是和用户线程并发的,再次强调是并发的,不是和G1的交替的暂停和运行的工作方式,注意这里的实现原理就是之前说的“Brooks Pointer”,同时使用**转发指针的操作+cas锁**将旧对象的引用修复为新对象的方式。这个阶段也是和G1最大的区别,实现了垃圾回收和用户线程的并发操作。
6. 初始引用更新:并发回收阶段复制对象结束之后,还需要把堆中的所有指向旧对象的更新到复制之后的新地址,这个操作也叫做**引用更新**。同样会产生一个**非常短暂的停顿**。
7. **并发引用更新:**真正开始引用更新的操作,时间长短取决于引用的多少。毫无疑问也是并发执行的
8. 最终引用更新:解决堆中引用更新之后,修正GC ROOT引用,这个阶段是最后一次停顿,停顿时间和GC Roots的数量有关。
9. 并发清理:最后回收没有任何对象的空Region

> 并发标记、并发回收、并发引用更新这三个阶段是最重要的,重点记忆即可。



下面的图是从官方的wiki扒过来的:

> Init Mark:初始标记
>
> Final Mark:最终标记
>
> Init-UR:初始引用更新
>
> Final-UR:最终引用更新

![并发标记、并发回收、并发引用](https://gitee.com/lazyTimes/imageReposity/raw/master/img/20210813101905.png)



## 结果对比

​        官方有一张对比图来显示Shenandoah的垃圾收集耗时对比,从图中可以看到做到了几乎无延迟的垃圾收集:

![](https://gitee.com/lazyTimes/imageReposity/raw/master/img/20210813102622.png)



# 总结

​        Shenadoah收集器是收款非JDK官方开发的收集器,然而很遗憾的是,因为商业竞争关系,他只存在于OpenJDK,没有被商用,并且后续由于更加ZGC的开发,Shenadoah的作用也在逐渐减少,但是不得不承认的作为没有JVM垃圾收集器开发经验的开发者们开发的收集器,这款收集器满足了要求并且十分值得借鉴和学习。

​        另外可以看到即使是简化工作原理,现代的垃圾收集器也已经十分复杂了,由于目前大部分开发者还是使用JDK8和G1等垃圾收集器,所以这些垃圾收集器在目前看来还是属于面向未来的收集器,但是毫无疑问我们需要不断的学习。



# 其他资料:

(https://wiki.openjdk.java.net/display/shenandoah/Main)

> Shenandoah收集器的JVM参数案例:`java -XX:+UseShenandoahGC -XX:ShenandoahGCHeuristics=passive -Xlog:gc`



# 写在最后

​        这本书讲述这款垃圾收集器的内容算是比较粗浅,但是对于我们了解这款收集器来说算是足够了。想要了解更多内容,个人建议直接找上面提供的官方WIKI入手,毕竟开发出来的人对这个东西才是最了解的。

burpliu 发表于 2021-8-13 11:22

楼主总结得很不错,学习了

Tamluo 发表于 2021-8-13 12:25


楼主总结得很不错,学习了{:301_1003:}

Days0708 发表于 2021-8-13 13:14

感谢大佬分享知识,先存起来再看!

吾爱破解鵬 发表于 2021-8-13 14:11

感谢分享

Mrstick 发表于 2021-11-16 00:17

这个可以,真不错{:1_921:}
页: [1]
查看完整版本: 深入理解JVM - Shenadoah