1. 低延迟垃圾收集器
衡量垃圾器的三个重要指标:
- 内存占用
- 吞吐量
- 延迟
这三个方面共同构成“不可能三角”,要在这三个方面同时具有卓越表现的收集器是非常困难的,甚至是不可能的,一款优秀的收集器最多同时可以达到其中两项。
低延迟收集器主要有Shenandoah和ZGC,它们有几个特点
- 几乎整个工作过程都是并发的,只有初始标记、最终标记阶段有短暂停顿
- 停顿时间基本是固定的,与堆的容量、堆中对象的数量不成正比例关系
- Shenadoah和ZGC都可以在任意管理的(ZGC实际只能管理4TB以内的堆)堆容量下,停顿不超过10ms
- 目前仍然处于实验室状态,被官方命名为低延迟垃圾收集器
2. Shenadoah垃圾收集器
Shenandoah是由RedHat公司独立发展的新型收集器项目,并于2014年贡献给OpenJDK,成为OpenJDK 12的正式特性之一。
Shenandoah目标是实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在10ms以内的垃圾收集器。相比于CMS和G1,不仅要进行并发的垃圾标记,还要并发地进行对象清理的后续动作。
2.1 Shenandoah和G1比较
Shenandoah更像是G1的继承者,有很多相同之处。
2.1.1 Shenandoah和G1相同点
- 基于Region的堆内存布局
- 用于存放大对象的Humongous Region
- 默认回收策略都是优先处理回收价值最高的Region
- 都支持并发的整理算法,初始标记并发标记等许多阶段的处理思路都高度一致
- Shenandoah和G1甚至共享部分实现代码,两者各自的优化改进都可能同步合并到对方
2.1.2 Shenandoah和G1不同点:
- G1收回阶段是多线程并行,但不能与用户线程并发;Shenandoah回收阶段是多线程并行,并且可以与用户线程并发
- G1有分专门的新生代和老年代Region,有实现分代;Shenandoah没有划分新生代和老年代Region,没有实现分代
- G1解决Region之间的跨代引用,使用了记忆卡,需要耗费大量内存和计算;Shenandoah用“连接矩阵”解决Region之间的跨代引用问题
2.2 Shenandoah工作阶段划分
- 初始标记
- 首先标记与GC Roots直接关联的对象
- stop the word,停顿时间与堆大小无关,只与GC Roots的数量有关
- 并发标记
- 与用户并发
- 遍历对象图,标记出全部可达对象
- 时间长短与堆在存活对象数量、对象图的结构复杂程度有关
- 最终标记
- 处理剩余的STAB扫描,统计回收价值最高的Region,将这些Region构成回收集
- 会有一小段stop the word
- 并发清理
- 清理整个区域内一个存活对象都没有的Region
- 并发回收
- 与其他HotSpot其他收集器的核心差异
- 要把回收集里的存活对象先复制一份到其他未被使用的Region
- 通过读屏障和转发指针解决复制对象和用户线程并行的问题。
- 初始引用更新
- 把堆中所有指向旧对象的引用修正到复制后的新地址
- 整个时间很短,还是会有一个非常短暂的stop the word。
- 并发引用更新
- 与用户线程并发
- 真正执行引用更新操作
- 时间长短与内存中涉及的引用数量多少有关;不需要沿着图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值。
- 最终引用更新
- 解决了堆中的引用更新后,还需要修正存在于GC Roots中的引用
- 最后一次stop the word,时间只与GC Roots有关
- 并发清理
- 并发回收已经没有存活对象的Region,供后续新对象分配使用
初始标记、并发标记、最终标记三个阶段都G1一样;上面9个阶段过程划分繁琐复杂,我们只要抓住三个最重要的并发阶段:并发标记、并发回收、并发引用更新。
2.3 转发指针
转发指针是Shenandoah支持并行整理的核心。
2.3.1 对象移动与用户程序并发的解决方案
- 内存保护陷阱(Memory Protection Trap)
- 在被移动对象原有的内存上设置保护陷阱,一旦用户程序访问归属于旧对象的内存空间就会产生自陷中段,进入预设好的异常处理器中,再由其中的代码逻辑把访问转发到复制后的新对象上
- 这种方案导致用户态频繁切换到内核态,代价很大,不能频繁使用
- 转发指针
- 与句柄定位一样,都是间接性的对象访问方式,差别是句柄通常统一存储在专门的句柄池中,转发指针分散放在每个对象头前面。
- 当对象拥有了新副本时,只需要修改旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新副本上。
- 实际上Shenandoah是通过CAS操作来保证并发时对象的访问正确性。
- Shenandoah通过设置读写屏障拦截对象访问操作,保证并发时原对象与复制对象访问的一致性;读屏障数量远比写屏障数量多,所以读屏障代价比写屏障代价更高。
- JDK13用“引用访问屏障”拦截对象中数据类型为引用类型的操作,而不去管原生数据类型等其他非引用字段的读写,能省去大量对原生类型、对象比较、对象加锁等场景中设置内存屏障所带来的消耗。
2.4 Shenandoah优缺点
- 优点:低延迟
- 缺点:高运行负担使得吞吐量下降
3. ZGC
ZGC收集器是一款基于Region内存布局的(暂时)不设分代的,使用了读屏障、染色打针、内存多重映射等技术来实现可并发标记-整理算法的,以低延迟为首要目标的一款新型垃圾收集器。
3.1 基于Region的内存布局
- 与Shenandoah和G1不一样,ZGC的Region具有动态性:动态创建、动态销毁、动态容量
- ZGC的Region可以有大、中、小三种类型的容量
- 小型Region:容量固定为2MB,用于放置小于256KB的小对象
- 中型Region:容易固定为32MB,用于放置大于等于256KB小于4MB的对象
- 大型Region:容易不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。
- 每个大于Region只会存放一个大对象,实际容量可能小于中型Region,最小容易可低到4MB
- 大于Region是不会被重分配的,因为复制一个大对象代价非常大
3.2 ZGC标志性设计——染色指针
染色指针是一种直接将少量额外的信息存储在指针上的技术。
- 64位系统中,ZGC利用64位中的低46位指针宽度,其中高4位存储标志信息,低42位存储地址空间
- 只有42位的地址空间,导致ZGC只能管理最大4TB的内存(2的42次幂)
3.2.1 染色指针的优势
- 一旦某个Region的存活对象移走之后,该Region就能被立即释放和重用,而不必等待整个堆中所有指针该Region的引用都被修正后才能清理
- 可以大幅减少垃圾收集过程中内存屏障的使用数量,实际上ZGC目前并未使用任何写屏障
- 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能
3.2.2 内存多重映射
染色指针重新定义内存中某些指针的其中几位,操作系统是否支持?这需要依赖多重映射技术解决内存寻址问题。
内存寻址模式:
- 实时寻址模式:所有进程共用一块物理内存空间,这样导致不同进程之间的内存无法相互隔离,一个进程污染了别的进程内存后,就只能整个系统复位后才能恢复。
- 保护寻址模式:处理器使用分页管理机制,把线性地址空间和物理地址空间分别划分为大小相同的块,这样的内存块被称为“页”;分页管理机制会建立线性地址空间到物理地址空间的映射表,进行线性地址到物理地址空间的映射。
Linux/x86-64平台的ZGC使用了多重映射将多个不同虚拟内存地址映射到同一个物理内存地址上,是多对一的映射关系。
3.2.3 ZGC工作阶段划分
- 并发标记
- 与G1、Shenandoah一样,遍历对象图做可达性分析,要经过初始标记、最终标记的短暂停顿,停顿阶段所做的事情的目标也类似。
- 与G1、Shenandoah不一样,ZGC的标记是在指针上而不是对象上进行,会更新染色指针中的Marked0、Marked1标志位
- 并发预备重分配
- 根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集。
- G1划分Region的目的是做收益优先的增量回收;ZGC每次回收都会扫描所有Region,省去G1中记忆集的维护成本。
- ZGC的重分配集决定了里面的存活对象会被重新复制到其他Region中,里面的Region会被释放
- 并发重分配
- 是ZGC执行过程中的核心阶段,反重分配集中的存活对象复制到新的Region上,并为重分配集的每个Region维护转发表,记录从旧对象到新对象的转向关系。
- Shenandoah使用转发指针,每次对象的访问都必须付出固定开销,每次都慢;ZGC的转发表使得ZGC拥有指针的“自愈”能力,使得只有第一次访问旧对象会陷入转发,也就是只慢一次。
- 由于染色指针的存在,一旦重分配集中某个Region存活对象都复制完毕后,这个Region就可以立即释放,哪怕堆中还有很多指向这个对象的未更新指针,因为这些旧指针一旦被使用,都是可“自愈”的。
- 并发重映射
- 跟Shenandoah并发引用更新阶段一样,修正整个堆中指向重分配集中旧对象的所有引用,主要目的是为了不变慢。
- 由于旧引用也是可以“自愈”的,最多只是第一次使用时多一次转发和修正,所以这个阶段并非必须的。
3.3 支持NUMA-Aware的内存分配
现代处理器因频率发展受限进而转身多核方向发展,原本在北桥芯片中的内存控制器被集成到了处理器内存中,这样每个处理器核心所在的裸晶都有属于自己内存管理器所管理的内存。
在NUMA架构下,ZGC收集器会优先尝试在请求线程当前所在处理器的本地内存上分配对象,以保证高效内存访问。
3.4 ZGC性能表现
在ZGC弱项吞吐量的表现
G1 < ZGC < Parallel Scavenge
在ZGC强项延迟的表现
G1 < Parallel Scavenge < ZGC