深入理解JVM - Shenadoah

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

image.png


牛很累,牛快要写不动了


前言


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


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


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


思维导图:


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


image.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收集器是放弃固定分代而是使用分区的设计,然而分区本质上还是分代的,只不过可以自由决定属于哪一个分代。


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


image.png


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


支持并发收集和整理


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


算法细节


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


brooks pointer


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


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


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


image.png


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


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


缺点


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


并发问题


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


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


image.png


结果对比


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


image.png


总结


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


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


其他资料:


Shenadoah垃圾收集器官网介绍

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


写在最后


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

相关文章
|
8月前
|
算法 Java Linux
深入理解JVM - Shenadoah
深入理解JVM - Shenadoah
90 1
|
算法 Oracle Java
深入理解JVM - Shenadoah
深入理解JVM - Shenadoah
161 0
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
384 1
|
3月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
49 4
|
12天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
|
1月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
2月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
2月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
27 3
|
2月前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
56 1
|
2月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。