《深入理解JVM》第三章垃圾收集器与回收分配策略(低延迟垃圾收集器)

完美的收集器的需要提高三个指标
1.内存占用 2.吞吐量 3.延迟
不过这仨被称为不可能三角,既然是三角,自然就不可能达成完美咯。
不过现今来说,低延迟的重要性日渐凸显。
现主要有两款垃圾收集器:

  • Shenandoah(仅用于Open JDK12之后版本)
  • ZCG(Oracle亲儿子)

以下大多图都是源自这个PDF
下图为各种主流垃圾回收器的并发情况(绿色为并发,黄色为stop the world)《深入理解JVM》第三章垃圾收集器与回收分配策略(低延迟垃圾收集器)_第1张图片

Shenandoah

与G1有很多相似之处。不同之处有三:、

  • 支持并发的整理算法
  • 默认不使用分代收集
  • 连接矩阵代替G1的双向卡表,所谓连接矩阵, 可简单理解为一个二维数组,如果Region N和Region M则在M行N列标记,这样回收器就可以通过它找到跨代的引用对象了。

并发的整理算法:
是不是很好奇怎么实现的,之前不能并发整理主要是涉及到了对象的移动,对象移动了用户线程中的引用就会找不到对象,它就“失恋”了(虽然还有其他对象,渣男),所以告诉我们不要异地恋,对象换个地方就找不到了,那么有没有办法将更改后的地方告知引用且让它不要从原地址去找那个对象了?Brooks Pointers转发指针读屏障就是干这活的。
之前实现类似的活是留个房东 (CPU) 在这 (设置保护陷阱),一旦用户访问对象原地址,房东就让他等等 (中断) ,他查一查对象的新地址 (转发到复制后的新对象地址) ,虽然可以实现,但是房东累不累啊 (频繁切换到核心态)
使用转发指针和读屏障就不一样了(虽然还是有转发的开销),转发指针就相当于有人 (收集器) 在原房门口 (读写屏障) 的门牌 (转发指针) 上改写为对象的新的地址。可是由于是同时进行 (并发) ,就有可能信还没改呢,引用就去找对象了 (竞争) ,那么就得出现找到的虽然长得基本上一模一样不是真正的对象,如果再发生一些不可描述的事情 (更新) 就难顶了,那咋整呢,要么对象还没搬走,要么就门牌信息已经改写 (CAS实现)
(转发指针在常态时是指向自己的)
工作流程分为9个阶段:

  • 初始标记: 同G1,标记与GC ROOT直接关联的对象,Stop The World
  • 并发标记: 同G1,与用户线程并发的遍历对象图
  • 最终标记: 同G1,使用**原始快照(SATB扫描)**的方式,并在此阶段标记最大收益的Region
  • 并发清理: 个别发现没有生存对象的Region这时便可以直接清理掉了
  • 并发回收: 并发的将存活对象复制到其他未使用的Region中,并发的实现是使用读屏障+转发指针(Brooks Pointers)来解决。
  • 初始化引用: 建立一个线程集合点,确保收集线程将对象移动任务已经完成,会有一个短暂的暂停
  • 并发引用更新:并发的按照物理地址的顺序线程搜索出引用类型,更新堆中引用(对象中的引用)。
  • 最终引用更新: 更新GC ROOT ,最后一次stop the world
  • 并发清理: 全部本次收集的region都“无人生还”了,并发清理。

下图是最主要的三个并发过程 (并发标记,并发回收,并发引用更新)

  • 绿色:还存活对象
  • 黄色:被选入回收的region
  • 蓝色:用户线程可以分配内存的region

《深入理解JVM》第三章垃圾收集器与回收分配策略(低延迟垃圾收集器)_第2张图片

ZGC

官方描述
与Shenandoah的目标高度相似,在堆吞吐量影响不大的前提下(与使用G1相比,吞吐量降低不超过15%)将任意大小堆GC暂停时间不应超过10ms。还支持“NUMA-Aware”内存分配(多核处理器,每个处理器所在的DIE都有属于自己内存,访问自己的内存会比较快,ZCG优先尝试在请求线程当前所在内存分配对象)。
使用的技术主要是读屏障、染色指针和内存重映射 等。

具有动态性的Region

  • 小型Region:固定为2M,放置小于256KB的小对象
  • 中型:固定为32M,存放256kb~4MB之间的对象
  • 大:只存放一个4MB以上的大对象,可动态变化,必须是2MB的整数倍

并发整理算法

为了实现上文说的对象搬家,不 去/成功 访问以前的地址的对象,而获得对象的信息,就实现了一个名为染色指针的玩意,它与之前的思路极其不同,其他都把对象的标记要么写在对象头里(Serial),要么用一片与对象互相独立的数据结构(比如G1,Shenandoah使用一种叫做BitMap,占据堆内存1/64的结构)来管理标记,而ZGC就不一样直接把标记信息写到引用指针上,被称为染色指针
染色指针 在64位系统中,将46位(因为linux的高16位不能用来寻址)指针用于存储这些信息,其中高4位存储四个标志信息,可以看到是否被标记装态(三色状态)和是否被移动过(进重分配表)以及是否只有通过finnalize()方法才能被访问到,这样ZGC可以管理的内存就只有4TB了(2(64-18-4)).
它的优势在于:

  • 染色指针使得某个Region存活对象被移走时立即将原内存释放掉(基于“自愈”特性)
  • 由于染色指针和没有分代,未使用任何写屏障,只使用了读屏障
  • 还有优化的空间,Linux下前18位是没有使用的,虽然无法寻址,不够如果把4位标志位写到那18位中,就可以扩到到64TB了。

由于染色指针的实现需要使用到虚拟内存映射技术,将多个不同的虚拟内存映射到同一物理地址,将染色指针按照标志位做分隔符,每段映射一个虚拟地址,使得那些虚拟地址都指向一个物理地址(称为多重映射),这样便可正常寻址了。

收集流程:
总共7个步骤,不过其中仨停顿与G1、Shenandoah相似。

  1. 开始标记: 检索与GC Root直接相连对象
  2. 并发标记 并发的标记对象…不对是引用(染色指针嘛)。
  3. 最终标记: 原始快照的方式处理错标引用。
  4. 并发预备重分配: ZGC每次回收都会扫描所有Region,将存活对象是否移动加入到重分配集中,决定被重新分配的Region原内存立即被释放,在JDK12后,类型卸载和弱引用的处理也是在这个阶段完成。
  5. 初始化重分配: 确保所有线程重分配表已完成。
  6. 并发重分配: 将存活对象复制到新的Region中,在重分配表中的对象所属的Region会维护一个转发表,记录旧对象向新对象转向关系,如果这时有引用访问被重分配的对象会被内存屏障捕获,通过转发表上的关系,转向访问新对象地址,并且会直接修改引用指向新对象(被称为自愈),好处是只会转发一次。
  7. 并发重映射: 修正整个堆中指向重分配集中旧对象的引用。由于自愈功能的存在,所以并不是个迫切的任务。所以ZGC巧妙的将其合并在下一阶段的并发标记中去做。一旦所有指针都被修正,该Region的转发表也就可以释放了。

劣势:

  1. 没有分代,整堆收集时间长
  2. 收集过程中产生的新对象死亡难以被回收,产生大量浮动垃圾

你可能感兴趣的:(JVM)