深刻理解JVM - Shenadoah

深刻理解JVM - Shenadoah

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战java

牛很累,牛快要写不动了算法

前言

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

​ 另外这两个垃圾收集器是彻底舍弃分代这个概念的,注意是彻底舍弃,并非相似G1收集器虽然使用了分区可是本质上仍是分代收集的收集器。markdown

​ 因为这两个收集器的内容较多,这里分开进行讲解,本篇讲解Shenadoah收集器。数据结构

思惟导图:

不想看文字的,能够查看思惟导图:www.mubucm.com/doc/7L4W-FA…并发

概述

低延迟垃圾收集器

​ 在正式介绍以前,有必要说明一下整个背景,现代的垃圾收集器考虑的点主要为下面这三个条件:内存占用,吞吐量,延迟,经过以前的收集器介绍,咱们知道了虽然主流的g1收集器在标记阶段实现了并发,可是在初始标记和筛选回收阶段仍是须要进行阶段性的stop world的,这个垃圾收集器并无作到真正意义上的并发,而且因为分区+region分代的设计限制,必然会产生垃圾收集的停顿。因此将来的垃圾收集器主要目标将会是面向极低延迟进军,也就是努力实现用户线程和垃圾收集器线程的彻底并发运行。oracle

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

g1和cms实现并发的细节

​ 以前的文章提到了增量更新原始快照,cms使用的是增量更新,g1使用的是原始快照,另外cms使用标记-清除的算法,免不了内存碎片,而g1虽然使用标记-整理,可是终究仍是须要进行暂停的,因此这是一个很是棘手的问题。jvm

Shenadoah

简介

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

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

特色

​ 下面来讲一下shenadoah的特色:

Region

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

没有分代和链接矩阵

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

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

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

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

支持并发收集和整理

​ 支持并发收集和整理,能够实现标记和整理阶段彻底和用户线程并发执行。

算法细节

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

brooks pointer

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

​ Brooks pointer的工做原理:就是在对象的结构布局上增长一个新的引用字段,这个引用一般状况下指向本身,当对象发生转移的时候,brooks pointer会指向新引用的地址,这样指向旧引用的对象就能够修复引用指向新对象,这种结构在形式上和JVM的句柄定位相似,都是使用一种间接的访问形式,差异是转发指针会分散存在对象头内部。(以前咱们讨论过对象头是动态扩展的格式)

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

补充:以前如何解决对象引用问题?

​ 使用的是一种在原有的对象内存之上设置保护陷阱+异常处理的方式,一旦出现访问旧对象的行为,就会进入到保护陷阱当中,而且进入异常处理器进行代码逻辑和引用的修复。这种方式看起来十分的有效,可是若是没有操做系统的支持,就须要经过不断的用户态到内核态的切换,须要耗费更多的上下文切换资源,也是一种很是耗费性能的妥协办法。

缺点

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

并发问题

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

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

​ 若是不防范这三个问题,就会致使用户线程的对象变动都是操做旧对象,因此必须针对指针的访问操做采起同步的措施。解决办法和对象的引用分配方式也是相似的也是使用CAS+更新失败重试的操做机制

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

读写屏障

​ shenandoah不只使用了写屏障还使用了读屏障,读屏障也是相似对象引用操做的一个AOP的切面,咱们都知道对象的读操做确定是要多于写操做的,因此使用读屏障的代价要大不少。

写屏障的概念能够看专栏以前的文章:深刻理解JVM - Hotspot算法细节#写屏障

​ 固然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:最终引用更新

并发标记、并发回收、并发引用

结果对比

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

总结

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

​ 另外能够看到即便是简化工做原理,现代的垃圾收集器也已经十分复杂了,因为目前大部分开发者仍是使用JDK8和G1等垃圾收集器,因此这些垃圾收集器在目前看来仍是属于面向将来的收集器,可是毫无疑问咱们须要不断的学习。

其余资料:

Shenadoah垃圾收集器官网介绍

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

写在最后

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

相关文章
相关标签/搜索