《深入理解Java虚拟机》(第三版)读书笔记(二):第三章 垃圾收集器与内存分配策略(下)

《深入理解Java虚拟机》(第三版)读书笔记(二):第三章 垃圾收集器与内存分配策略(上)

HotSpot的算法细节实现

​ ​ ​ ​ ​ ​ 把这部分内容放到第三章的读书笔记(下)是为了方便理解各种垃圾收集器。

根节点枚举

​ ​ ​ ​ ​ ​ 所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到根节点枚举的目的。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。

安全点

​ ​ ​ ​ ​ ​ 有了安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。因此,安全点的选定既不能太少以至于让收集器等待时间过长,也不能太过频繁以至于过分增大运行时的内存负荷。安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。

​ ​ ​ ​ ​ ​ 在垃圾收集发生时让所有线程都跑到最近的安全点然后停顿下来有2种方案,一个是抢先式中断,还有主动式中断。

安全区域

​ ​ ​ ​ ​ ​ 安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。安全点机制保证的是程序执行的时候,程序不执行的时候,也就是没有分配处理器的时间,就需要安全区域开解决问题。

记忆集和卡表

​ ​ ​ ​ ​ ​ 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节。

​ ​ ​ ​ ​ ​ 记录精度有字长精度、对象精度和卡精度,卡精度所指的是一种称为卡表的方式去实现记忆集,卡表是记忆集的一种具体实现,定义了记忆集的记录精度、与堆内存的映射关系等等。

写屏障

​ ​ ​ ​ ​ ​ 通过写屏障维护卡表状态,引用类型字段赋值的前后都在写屏障的范围内,赋值前的部分的写屏障叫做写前屏障,赋值后的叫做写后屏障。G1收集器之前,其他收集器只用到了写后屏障。

经典垃圾收集器

​ ​ ​ ​ ​ ​ 下图展示了7种作用域不同分代的收集器,连线说明可以搭配使用。CMS 和 G1是重点介绍的收集器。

在这里插入图片描述

Serial收集器

​ ​ ​ ​ ​ ​ 单线程收集器,除了只会使用一个处理器或一条收集线程去完成垃圾收集工作外,更重要的是强调在它进行垃圾收集的时候,必须暂停其他所有工作线程,直到收集结束。

​ ​ ​ ​ ​ ​ 到目前为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,见到而且高效,在内存资源受限的环境里,是所有收集器里额外内存消耗最小的。

ParNew收集器

​ ​ ​ ​ ​ ​ 实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集外,其余性威包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。

​ ​ ​ ​ ​ ​ 除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS是老年代的收集器,没法和新生代收集器Parallel Scanvenge配合,所以使用CMS收集老年代的时候,新生代只能选择ParNew或者Serial中的一个。

​ ​ ​ ​ ​ ​ 后来出现了G1,这个可以说是代替CMS,它是一个面向全堆的收集器,不需要其他新生代收集器的配合工作。自JDK9开始,ParNew加CMS收集器的组合就不是官方推荐的服务端模式下的收集器解决方案了,官方希望G1能完全取代。

注意:并行和并发这两个概念得区分,并行说的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同 工作,通常默认此时用户线程是处于等待状态;并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程和用户线程都在运行,应用程序仍然能响应服务请求但是处理的吞吐量收到一定影响。

Parallel Scavenge收集器

​ ​ ​ ​ ​ ​ 基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。

Serial Old收集器

​ ​ ​ ​ ​ ​ Serial收集器的老年代版本,也是供客户端模式下的HotSpot虚拟机使用,在服务端模式下,可能有2种用途:在JDK5之前的版本中和Parallel Scavenge收集器搭配使用,另外就是作为CMS收集器发生失败时的后备预案。

Parallel Old收集器

​ ​ ​ ​ ​ ​ Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

CMS收集器

​ ​ ​ ​ ​ ​ CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间(低停顿)为目标的收集器。基于标记-清除算法实现的,整个收集过程分为:初始标记、并发标记、重新标记和并发清除四个阶段。

​​ ​ ​ ​ ​ ​ 在整个阶段中耗时最长的是并发标记和并发清楚,CMS收集器的内存回收过程是与用户线程一起并发执行的。

​ ​ ​ ​ ​ ​ 缺点的话,比如说对处理器资源敏感、无法处理浮动垃圾、收集结束会有大量的空间碎片产生(主要是由标记-清除算法引发的)。

Garbage First收集器

​ ​ ​ ​ ​ ​ 这个收集器我们经常简称为G1,是发展历史上的里程碑成果,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。从整体上看,是基于标记-整理算法实现的收集器,但从局部(两个Region之间)上看又是基于标记-复制实现的。anyway,G1在运行期间不会产生空间碎片。

​ ​ ​ ​ ​ ​ G1是一款主要面向服务端应用的垃圾收集器,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。

低延迟垃圾收集器

​ ​ ​ ​ ​ ​ 衡量垃圾收集器的三项重要指标分别是内存占用、吞吐量和延迟。

Shenandoah收集器[JDK 12]

​ ​ ​ ​ ​ ​ 相比于G1,主要的改进优点:

  1. 支持并发的整理算法,Shenandoah的回收阶段可以和用户线程并发执行;
  2. Shenandoah 目前不使用分代收集,也就是没有年轻代老年代的概念在里面了;
  3. Shenandoah 摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率。

​ ​ ​ ​ ​ ​ Shenandoah使用转发指针和读屏障来实现并发整理。

读屏障用于保证读操作有序。屏障之前的读操作一定会先于屏障之后的读操作完成,写操作不受影响。

复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。Brooks Pointer 转发指针技术是来实现对象移动与用户程序并发的一种解决方案。在正常不处于并发移动的情况下,引用指向对象自己,在对象移动的时候只需要将Pointer指向新对象。

​ ​ ​ ​ ​ ​ Shenandoah收集器的工作过程一共有九个阶段:

​ ​ ​ ​ ​ ​ 初始标记(Init Mark): 启动并发标记。它为堆和应用程序线程准备并发标记,然后扫描根集。这是流程中的第一个暂停,最主要的消耗是根集扫描。因此,其持续时间取决于根集大小。暂停时间和堆大小无关,只和GC Roots数量有关系。

​ ​ ​ ​ ​ ​ 并发标记(Concurrent Marking): 遍历堆,并跟踪可访问的对象(三色标记对象)。此阶段与应用程序一起运行,其持续时间取决于活动对象的数量和堆中对象关系的结构。由于应用程序可以在此阶段自由分配新数据,因此在并发标记期间堆占用率会上升。这里主要运用的是SATB(snapshot-at-the-beginning)

​ ​ ​ ​ ​ ​ 最终标记(Final Mark): 通过清理所有等待的标记,更新队列,重新扫描根设置三个并发的来完成标记。(这里主要是处理一些SATB没有处理完的引用)它还通过确定要撤离的区域(收集集合),预先疏散一些根来初始化疏散,并且通常为下一阶段准备运行时间。这项工作的一部分可以在Concurrent Cleanup阶段同时完成。这是周期中的第二个暂停,这里消耗最主要的时间是清理队列并扫描根集。

​ ​ ​ ​ ​ ​ 并发清理(Concurrent Cleanup):用于清理那些整个区域内连一个存活对象都没有找到的Region,这类Region被称为立即垃圾区域(Immediate Garbage Region)。 即在并发标记之后检测到的没有活动对象的区域。

​ ​ ​ ​ ​ ​ 并发回收(Concurrent Evacuation): 将对象集合从集合集复制到其他区域。这是与其他OpenJDK GC的主要区别。此阶段再次与应用程序一起运行,因此应用程序可以自由分配。其持续时间取决于为流程选择的集合集的大小。

​ 初始引用更新(Init Update References): 初始化更新引用阶段。除了确保所有GC和应用程序线程都已完成疏散,然后为下一阶段准备GC之外,它几乎没有任何作用。这是周期中的第三次暂停,最短暂停。

​ ​ ​ ​ ​ ​ 并发引用更新(Concurrent Update References): 真正开始进行引用更新操作,该阶段是和用户线程一起并发的,时间长短取决于 内存中涉及的引用数量的多少,并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,旧值改成新值。

​ ​ ​ ​ ​ ​ 最终引用更新(Final Update Refs ):解决了堆中的引用更新后,还要修正存在于GC Roots中的引用,这个阶段是最后一次停顿,停顿时间只与GC Roots的数量有关系。

​ ​ ​ ​ ​ ​ 并发清理(Concurrent Cleanup):整个回收集中所有的Region已再无存活对象,这些Region都变成Immediate Garbage Regions了,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。

​ ​ ​ ​ ​ ​ 下图是最核心地三个阶段地过程,即并发标记、并发回收、并发引用更新。

img

​ ​ ​ ​ ​ ​ 这个收集器的优点是延迟低,缺点是高运行负担会使吞吐量下降,使用读屏障,会增大系统的性能开销。

ZGC收集器[JDK 11]

​ ​ ​ ​ ​ ​ 逻辑上依次将ZGC分为Mark(标记)、Relocate(迁移)、Remap(重映射)三个阶段。

Mark: 所有活的对象都被记录在对应Page的Livemap(活对象表,bitmap实现)中,以及对象的Reference(引用)都改成已标记(Marked0或Marked1)状态,参考染色指针。

Relocate: 根据页面中活对象占用的大小选出的一组Page,将其中中的活对象都复制到新的Page, 并在额外的forward table(转移表)中记录对象原地址和新地址对应关系。

Remap: 所有Relocated的活对象的引用都重新指向了新的正确的地址。

  • 染色指针

img

Marked0/marked1: 判断对象是否已标记

Remapped: 判断应用是否已指向新的地址

Finalizable: 判断对象是否只能被Finalizer访问

​ ​ ​ ​ ​ ​ 为什么会存在2个mark标记?

->每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。

->GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。

->GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。

​ ​ ​ ​ ​ ​ ZGC最大的问题是浮动垃圾,优点则是低停顿、高吞吐量,收集过程中额外耗费的内存比较小。ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾

​​ ​ ​ ​ ​ ​ 解决的方案就是增大堆容量,但是这个方法不能从根本上解决问题,要想从根本解决问题就得需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。

关于ZGC垃圾收集器,建议看看下面这篇博客:

英文版:https://dinfuehr.github.io/blog/a-first-look-into-zgc/

中文翻译版:https://www.cnblogs.com/ctgulong/p/9742434.html

发布了58 篇原创文章 · 获赞 5 · 访问量 6259

猜你喜欢

转载自blog.csdn.net/weixin_40992982/article/details/104045332