jjzjj

从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC

Jcloud 2024-01-13 原文

作者:京东科技 康志兴

Shenandoah

Shenandoah一词来自于印第安语,十九世纪四十年代有一首著名的航海歌曲在水手中广为流传,讲述一位年轻富商爱上印第安酋长Shenandoah的女儿的故事。 后来美国有一条位于Virginia州西部的小河以此命名,所以Shenandoah的中文译名为“情人渡”。

Shenandoah首次出现在Open JDK12中,是由Red Hat开发,主要为了解决之前各种垃圾回收器处理大堆时停顿较长的问题。

相比较G1将低停顿做到了百毫秒级别,Shenandoah的设计目标是将停顿压缩到10ms级别,且与堆大小无关。它的设计非常激进,很多设计点在权衡上更倾向于低停顿,而不是高吞吐。

“G1的继承者”

Shenandoah是OpenJDK中的垃圾处理器,但相比较Oracle JDK中根正苗红的ZGC,Shenandoah可以说更像是G1的继承者,很多方面与G1非常相似,甚至共用了一部分代码。

总的来说,Shenandoah和G1有三点主要区别:

1.G1的回收是需要STW的,而且这部分停顿占整体停顿时间的80%以上,Shenandoah则实现了并发回收。

2.Shenandoah不再区分年轻代和年老代。

3.Shenandoah使用连接矩阵替代G1中的卡表。

关于G1的详细介绍请翻看前一篇:从原理聊JVM(二):从串行收集器到分区收集开创者G1

连接矩阵(Connection Matrix)

G1中每个Region都要维护卡表,既耗费计算资源还占据了非常大的内存空间,Shenandoah使用了连接矩阵来优化了这个问题。

连接矩阵可以简单理解为一个二维表格,如果Region A中有对象指向Region B中的对象,那么就在表格的第A行第B列打上标记。

比如,Region 1指向Region 3,Region 4指向Region 2,Region 3指向Region 5:

相比G1的记忆集来说,连接矩阵的颗粒度更粗,直接指向了整个Region,所以扫描范围更大。但由于此时GC是并发进行的,所以这是通过选择更低资源消耗的连接矩阵而对吞吐进行妥协的一项决策。

转发指针

转发指针的性能优势

想要达到并发回收,就需要在用户线程运行的同时,将存活对象逐步复制到空的Region中,这个过程中就会在堆中同时存在新旧两个对象。那么如何让用户线程访问到新对象呢?

此前,通常是在旧对象原有内存上设置保护陷阱(Memory Protection Trap),当访问到这个旧对象时就会发生自陷异常,使程序进入到预设的异常处理器中,再由处理器中的代码将访问转发到复制后的新对象上。

自陷是由线程发起来打断当前执行的程序,进而获得CPU的使用权。这一操作通常需要操作系统参与,那么就会发生用户态到内核态的转换,代价十分巨大。

所以Rodney A.Brooks提出了使用转发指针来实现通过旧对象访问新对象的方式:在对象头前面增加一个新的引用字段,在非并发移动情况下指向自己,产生新对象后指向新对象。那么当访问对象的时候,都需要先访问转发指针看看其指向哪里。虽然和内存自陷方案相比同样需要多一次访问转发的开销,但是前者消耗小了很多。

转发指针的问题

转发指针主要存在两个问题:修改时的线程安全问题和高频访问的性能问题

1.对象体增加了一个转发指针,这个指针的修改和对象本身的修改就存在了线程安全问题。如果通过被访问就可能发生复制了新对象后,转发对象修改之前发生了旧对象的修改,这就存在两个对象不一致的问题了。对于这个问题,Shenandoah是通过CAS操作来保证修改正确性的。

2.转发指针的加入需要覆盖所有对象访问的场景,包括读、写、加锁等等,所以需要同时设置读屏障和写屏障。尤其读操作相比单纯写操作出现频率更高,这样高频操作带来的性能问题影响巨大。所以Shenandoah在JDK13中对此进行了优化,将内存屏障模型改为引用访问屏障,也就是说,仅仅在对象中引用类型的读写操作增加屏障,而不去管原生对象的操作,这就省去了大量的对象访问操作。

Shenandoah的运行步骤

  1. 初始标记(Init Mark)[STW] [同G1]

标记与GC Roots直接关联的对象。

  1. 并发标记(Concurrent Marking)[同G1]

遍历对象图,标记全部可达对象。

  1. 最终标记(Final Mark)[STW] [同G1]

处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集。

  1. 并发清理(Concurrent Cleanup)

回收所有不包含任何存活对象的Region(这类Region被称为Immediate Garbage Region)。

  1. 并发回收(Concurrent Evacuation)

将回收集里面的存货对象复制到一个其他未被使用的Region中。并发复制存活对象,就会在同一时间内,同一对象在堆中存在两份,那么就存在该对象的读写一致性问题。Shenandoah通过使用转发指针将旧对象的请求指向新对象解决了这个问题。这也是Shenandoah和其他GC最大的不同。

  1. 初始引用更新(Init Update References)[STW]

并发回收后,需要将所有指向旧对象的引用修正到新对象上。这个阶段实际上并没有实际操作,只是设置一个阻塞点来保证上述并发操作均已完成。

  1. 并发引用更新(Concurrent Update References)

顺着内存物理地址线性遍历堆空间,更新并发回收阶段复制的对象的引用。

  1. 最终引用更新(Final Update References)[STW]

堆空间中的引用更新完毕后,最后需要修正GC Roots中的引用。

  1. 并发清理(Concurrent Cleanup)

此时回收集中Region应该全部变成Immediate Garbage Region了,再次执行并发清理,将这些Region全部回收。

ZGC

ZGC是Oracle官方研发并JDK11中引入,并于JDK15中作为生产就绪使用,其设计之初定义了三大目标:

1.支持TB级内存

2.停顿控制在10ms以内,且不随堆大小增加而增加

3.对程序吞吐量影响小于15%

随着JDK的迭代,目前JDK16及以上版本,ZGC已经可以实现不超过1毫秒的停顿,适用于堆大小在8MB到16TB之间。

ZGC的内存布局

ZGC和G1一样也采用了分区域的堆内存布局,不同的是,ZGC的Region(官方称为Page,概念同G1的Region)可以动态创建和销毁,容量也可以动态调整。

ZGC的Region分为三种:

1.小型Region容量固定为2MB,用于存放小于256KB的对象。

2.中型Region容量固定为32MB,用于存放大于等于256KB但不足4MB的对象。

3.大型Region容量为2MB的整数倍,存放4MB及以上大小的对象,而且每个大型Region中只存放一个大对象。由于大对象移动代价过大,所以该对象不会被重分配。

重分配集(Relocation Set)

G1中的回收集用来存放所有需要G1扫描的Region,而ZGC为了省去卡表的维护,标记过程会扫描所有Region,如果判定某个Region中的存活对象需要被重分配,那么就将该Region放入重分配集中。

通俗的说,如果将GC分为标记和回收两个主要阶段,那么回收集是用来判定标记哪些Region,重分配集用来判定回收哪些Region

染色指针

和Shenandoah相同,ZGC也实现了并发回收,不同的是前者是使用转发指针来实现的,后者则是采用染色指针的技术来实现。

三色标记本质上与对象无关,仅仅与引用有关:通过引用关系判定对像存活与否。HotSpot虚拟机中不同垃圾回收器有着不同的处理方式,有些是标记在对象头中,有些是标记在单独的数据结构中,而ZGC则是直接标记在指针上。

64位机器指针是64位,Linux下64位中高18位不能用来寻址,剩下46位中,ZGC选择其中4位用来辅助GC工作,另外42位能够支持最大内存为4T,通常来说,4T的内存完全够用。

具体来说,ZGC在指针中增加了4个标志位,包括FinalizableRemappedMarked 0Marked 1

源码注释如下:

 6                 4 4 4  4 4                                             0
 3                 7 6 5  2 1                                             0
+-------------------+-+----+-----------------------------------------------+
|00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
+-------------------+-+----+-----------------------------------------------+
|                   | |    |
|                   | |    * 41-0 Object Offset (42-bits, 4TB address space)
|                   | |
|                   | * 45-42 Metadata Bits (4-bits)  0001 = Marked0
|                   |                                 0010 = Marked1
|                   |                                 0100 = Remapped
|                   |                                 1000 = Finalizable
|                   |
|                   * 46-46 Unused (1-bit, always zero)
|
* 63-47 Fixed (17-bits, always zero)


Finalizable标识表示对象是否只能通过finalize()方法访问到,RemappedMarked 0Marked 1用作三色标记(后面简称为M0M1)。

为什么既有M0还有M1呢?

因为ZGC标记完成后并不需要等待对象指针重映射就可以进行下一次垃圾回收循环,也就是说两次垃圾回收的全过程是有重叠的,所以使用两个标记位分别用作两次相邻GC过程的标记,M0M1交替使用。

染色指针的在GC过程中的作用

我们通过红蓝黄三个颜色分别表示三种标记状态:

1.第一次标记开始时所有的指针都处于Remapped状态

  1. 从GC Root开始,顺着对象图遍历扫描,存活对象标记为M0

  1. 标记完成后,开始进行并发重分配。最终目标是将A、B、C三个存活对象都移动到新的Region中去。

整个标记过程中新分配到对象都被直接标记为M0,比如对象D。

复制完成的对象,指针就可以由M0改为Remapped,并将旧对象到新对象到映射关系保存到转发表中。

  1. 如果此时系统访问对象C,会触发读屏障,将原引用修正到新的对象C的地址上去,并转发访问,最后删除转发表的记录。

这个行为称为指针的“自愈”。

实际上,如果没有对象D的存在,在上一步所有存货对象转移完成后,旧的Page就可以被回收了,依靠指针和转发表就可以将所有访问转发到新的Page中去。

  1. 并发重映射阶段会把所有引用修正,并删除转发表的记录。

  1. 下一次并发标记开始后,由于上一次垃圾回收循环并没有完成,所以Remapped指针被标记为M1,用来和上一次的存活对象标记作区分。

可以看出,并发标记的过程中,ZGC是通过读屏障来保证访问的正确转发,并且由于染色指针采用惰性更新的策略,相比Shenandoah每次都要先访问转发指针的两次寻址来说快上不少。

染色指针的三大优点

1.由于染色指针提供的“自愈”能力,当某个Page被清除后可以立刻被回收,而无需等待修正全部指向该Page的引用。

2.ZGC完全不需要使用写屏障,原因有二:由于使用染色指针,无需更新对象体;没有分代所以无需记录跨代引用。

3.染色指针并未完全开发使用,剩下的18位提供了非常大的扩展性。

而染色指针有一个天然的问题,就是操作系统和处理器并不完全支持程序对指针的修改。

多种内存映射

染色指针只是JVM定义的,操作系统、处理器未必支持。为了解决这个问题,ZGC在Linux/x86-64平台上采用了虚拟内存映射技术。

ZGC为每个对象都创建了三个虚拟内存地址,分别对应RemappedMarked 0Marked 1,通过指针指向不同的虚拟内存地址来表示不同的染色标记。

分代

ZGC没有分代,这一点并不是技术权衡,而是基于工作量的考虑。所以目前来看,整体的GC效率还有很大提升空间。

读屏障

ZGC使用了读屏障来完成指针的“自愈”,由于ZGC目前没有分代,且ZGC通过扫描所有Region来省去卡表使用,所以ZGC并没有写屏障,这成为ZGC一大性能优势。

NUMA

多核CPU同时操作内存就会发生争抢,现代CPU把内存控制系统器集成到处理器内核中,每个CPU核心都有属于自己的本地内存。

在NUMA架构下,ZGC会有现在自己的本地内存上分配对象,避免了内存使用的竞争。

在ZGC之前,只有Parallet Scavenge支持NUMA内存分配。

ZGC的运行步骤

ZGC和Shenadoah一样,几乎所有运行阶段都和用户线程并发进行。其中同样包含初始标记、重新标记等STW的过程,作用相同,不再赘述。重点介绍以下四个并发阶段:

并发标记

并发标记阶段和G1相同,都是遍历对象图进行可达性分析,不同的是ZGC的标记在染色指针上。

并发预备重分配

在这个阶段,ZGC会扫描所有Region,如果哪些Region里面的存活对象需要被分配的新的Region中,就将这些Region放入重分配集中。

此外,JDK12后ZGC的类卸载和弱引用的处理也在这个阶段。

并发重分配

ZGC在这个阶段会将重分配集里面的Region中的存货对象复制到一个新的Region中,并为重分配集中每一个Region维护一个转发表,记录旧对象到新对象的映射关系。

如果在这个阶段用户线程并发访问了重分配过程中的对象,并通过指针上的标记发现对象处于重分配集中,就会被读屏障截获,通过转发表的内容转发该访问,并修改该引用的值。

ZGC将这种行为称为自愈(Self-Healing),ZGC的这种设计导致只有在访问到该指针时才会触发一次转发,比Shenandoah的转发指针每次都要转发要好得多。

另一个好处是,如果一个Region中所有对象都复制完毕了,该Region就可以被回收了,只要保留转发表即可。

并发重映射

最后一个阶段的任务就是修正所有的指针并释放转发表。

这个阶段的迫切性不高,所以ZGC将并发重映射合并到在下一次垃圾回收循环中的并发标记阶段中,反正他们都需要遍历所有对象。

总结

现代的垃圾回收器为了低停顿的目标可谓将“并发”二字玩到极致,Shenandoah在G1基础上做了非常多的优化来使回收阶段并行,而ZGC直接采用了染色指针、NUMA等黑科技,目的都是为了让Java开发者可以更多的将精力放在如何使用对象让程序更好的运行,剩下的一切交给GC,我们所做的只需享受现代化GC技术带来的良好体验。

参考:

1.OpenJDK 17 中的 Shenandoah:亚毫秒级 GC 停顿【译】 - 知乎 (zhihu.com)

2.https://shipilev.net/talks/devoxx-Nov2017-shenandoah.pdf

3.https://openjdk.java.net/jeps/333

有关从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC的更多相关文章

  1. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  2. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  3. ruby - 现代计算机的功能是否不足以处理字符串而无需使用符号(在 Ruby 中) - 2

    我读过的关于Ruby符号的每一篇文章都在谈论符号相对于字符串的效率。但是,这不是1970年代。我的电脑可以处理一些额外的垃圾收集。我错了吗?我拥有最新最好的奔腾双核处理器和4GBRAM。我认为这应该足以处理一些字符串。 最佳答案 您的计算机可能能够处理“一点点额外的垃圾收集”,但是当“一点点”发生在运行数百万次的内部循环中时呢?如果它在内存有限的嵌入式系统上运行呢?有很多地方你可以随意使用字符串,但在某些地方你不能。这完全取决于上下文。 关于ruby-现代计算机的功能是否不足以处理字符串

  4. ruby-on-rails - 为什么 Devise/Omniauth 会向 URL 添加垃圾? - 2

    使用facebook登录后,我被重定向到/#_=_,其中显示主页。这种垃圾也出现在其他URL中,例如当注册失败并被重定向到/users/sign_in#_=_为什么会发生这种情况,我该如何解决? 最佳答案 如果你真的不想要它,一些简单的javascript就可以了:if(window.location.hash=="#_=_"){window.location.hash="";} 关于ruby-on-rails-为什么Devise/Omniauth会向URL添加垃圾?,我们在StackO

  5. ruby-on-rails - ActionMailer HTML 编码 hell - 特殊字符替换为垃圾 - 2

    我有UTF-8字符串:Website•Facebook那是中间的一颗子弹又名•或0xE20x800xA2此值已正确存储在数据库中,并使用默认设置使用Rails3和ruby​​1.9.3正确显示在屏幕上。我正在尝试通过HTML电子邮件发送此邮件,但是当一切都说完之后,接收端看到的是垃圾:这背后的代码很简单,我有一个ActionMailer子类(默认使用UTF-8)设置以在布局中发送带有UTF-8内容编码的HTML电子邮件:email.html.erb布局文件:"all"%>内容使用与呈现网页相同的View,重要的一行是:我已经尝试了很多很多force_encoding的排列,e

  6. ruby - 符号的垃圾收集 Ruby 2.2.1 - 2

    所以从Ruby2.2+版本开始引入了符号垃圾回收。我在irb中编写了以下代码片段:before=Symbol.all_symbols.size#=>3331100_000.timesdo|i|"sym#{i}".to_symendSymbol.all_symbols.size#=>18835GC.startSymbol.all_symbols.size#=>3331因此,正如预期的那样,它收集了使用to_sym动态生成的所有符号。那么GC是如何知道收集哪些符号的呢?即使它们在程序中被引用,它会收集符号吗?符号垃圾回收是如何工作的?如果我创建的其中一个符号在程序中被引用,它还会收集它吗?

  7. 【Unity游戏破解】外挂原理分析 - 2

    文章目录认识unity打包目录结构游戏逆向流程Unity游戏攻击面可被攻击原因mono的打包建议方案锁血飞天无限金币攻击力翻倍以上统称内存挂透视自瞄压枪瞬移内购破解Unity游戏防御开发时注意数据安全接入第三方反作弊系统外挂检测思路狠人自爆实战查看目录结构用il2cppdumper例子2-森林whoishe后记认识unity打包目录结构dll一般很大,因为里面是所有的游戏功能编译成的二进制码游戏逆向流程开发人员代码被编译打包到GameAssembly.dll中使用il2ppDumper工具,并借助游戏名_Data\il2cpp_data\Metadata\global-metadata.dat

  8. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  9. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  10. 最强Http缓存策略之强缓存和协商缓存的详解与应用实例 - 2

    HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca

随机推荐