GC 种类介绍

两个收集器间有连线,表明它们可以搭配使用

  • Serial/Serial Old
  • Serial/CMS
  • ParNew/Serial Old
  • ParNew/CMS
  • Parallel Scavenge/Serial Old
  • Parallel Scavenge/Parallel Old
  • Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案

老一代 GC

CMS

安全点

各种线程进入安全点的示意图

线程分类 描述
1 G1线程,如refine线程,并发标记线程 在进入安全点时他们会暂停,但是每个线程暂停的时间并不确定,依赖于每个线程join和leave的时间
2 Java线程,执行解释代码 在进入安全点时会暂停,但每个线程暂停的时间不一定,依赖于每个线程正在执行的字节码
3 Java线程,执行编译代码 在进入安全点时会暂停,但每个线程暂停的时间不一定,只有线程执行了return或者安全点指令才能进入
4 一直运行的本地代码线程 在进入安全点时不会暂停
5 从本地代码返回执行Java代码的线程 在进入安全点时不会暂停,而是在返回的时进行暂停,通过这个时GC正在执行VM操作

参考

G1

String.intern(),以及 G1 的字符串去重功能对比

ZGC

GC 分为 6 个步骤

  • 初始标记,STW
  • 并发标记
  • 再标记(重新标记),STW
  • 并发准备转移
  • 初始转移,STW
  • 并发转移

另一个视角,其中有三个 STW

  • pause mark start,也就是初始标记
  • pause mark end,重新标记
  • pause relocate start,初始转移

使用 mmap,将多个虚拟内存地址,指向同一个物理地址

  • [0~4TB) 对应Java堆
  • [4TB ~ 8TB) 称为M0地址空间
  • [8TB ~ 12TB) 称为M1地址空间
  • [12TB ~ 16TB) 预留未使用,[16TB ~ 20TB) 称为Remapped空间

ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效

ZGC实际仅使用64位地址空间的第0~41位,而第42~45位存储元数据,第47~63位固定为0。

读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码

1
2
3
4
5
Object o = obj.FieldA   // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o  // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i =  obj.FieldB  //无需加入屏障,因为不是对象引用

分代回收

  • 网上找的一些中文资料,将 ZGC分为 三种region,小于 2M,小于 32M,以及 > 32M
  • 但根据 youtub 上的介绍(他们是在Oracle工作就是搞Java虚拟机的,肯定更权威),实际是分代的
  • 分为 minor GC,以及 major GC,前者是收集年轻代,后者是收集整个 heap
  • 这个分代可能是出现在更高版本的 JDK 中
  • 对比 cassandra,可以提供 4倍吞吐量,只需要 1/4 内存使用
  • 暂停时间 小于 1ms
  • 分代 GC 的颜色指针 不再是 4个,可以扩展到 12个,支持更复杂的算法
  • 可以去掉年轻代、老年代大小,可以自动调整;可以去掉并行线程,可以自动调整

分代 ZGC 的读写屏障

一次垃圾收集过程中 地址视图的切换过程

  • 初始化:ZGC初始化之后,整个内存空间的地址视图被设置为Remapped。程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动,此时进入标记阶段
  • 并发标记阶段:第一次进入标记阶段时视图为M0,如果对象被GC标记线程或者应用线程访问过,那么就将对象的地址视图从Remapped调整为M0。所以,在标记阶段结束之后,对象的地址要么是M0视图,要么是Remapped。如果对象的地址是M0视图,那么说明对象是活跃的;如果对象的地址是Remapped视图,说明对象是不活跃的。
  • 并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为Remapped。如果对象被GC转移线程或者应用线程访问过,那么就将对象的地址视图从M0调整为Remapped

之所以设计成两个,是为了区别前一次标记和当前标记。也即,第二次进入并发标记阶段后,地址视图调整为M1,而非M0

着色指针和读屏障技术不仅应用在并发转移阶段,还应用在并发标记阶段

  • 将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中
  • 而在ZGC中,只需要设置指针地址的第42~45位即可,并且因为是寄存器访问,所以速度比访问内存更快

ZGC 中的暂停场景

  • GC时,初始标记:日志中Pause Mark Start。
  • GC时,再标记:日志中Pause Mark End。
  • GC时,初始转移:日志中Pause Relocate Start。
  • 内存分配阻塞:当内存不足时线程会阻塞等待GC完成,关键字是”Allocation Stall”。
  • 安全点:所有线程进入到安全点后才能进行GC,ZGC定期进入安全点判断是否需要GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。
  • dump线程、内存:比如jstack、jmap命令。

参考链接

ShenandoahGC

这种 GC 分为好几个阶段

  • 初始标记,STW
  • 并发标记,跟 ZGC类似
  • 最终标记,STW,也就是重新标记,这里要扫描引用队列和root set
  • 并发清理,对于整个region都标记为垃圾的,可以立刻清理掉
  • 并发疏散,将存活的对象拷贝到其他region,这点是跟其他 GC 不同的地方,这里需要拷贝存活对象会很多,但可以并行
  • 初始化更新引用, STW,确认所有的 GC 和应用线程都完成了疏散,最短的 STW
  • 并发更新引用,更新引用的对象,这些对象在并非疏散的时候已经被转移走了,这也是跟其他 GC 不同的地方
  • 最终更新引用,STW,重新扫描 root set
  • 并发清理,回收所有没有引用的region
    所以 Shenandoah 会有 4个 STW
  • 初始标记
  • 重新标记
  • 初始化更新引用
  • 最终更新引用

这里每次 STW 基本都跟 root set大小有关,跟 对象大小无关
所以只要保证 root set 不要太大,STW 的时间就是可控的

并发标记,采用了 brooks points 方式

  • 将原对象拷贝一份,到目的地,brooks points也被拷贝了
  • 这个 brooks points 指向了对象头,当拷贝完后 老的brooks指向老的头,新的brooks指向新的头
  • 然后使用 CAS,将老的brooks指针,指向新的对象头
  • 后面如果有应用访问老的对象,就会通过brooks指针,指向新对象了

屏障

  • 通过新引用更新直接使用新的brooks指针访问即可
  • 通过老引用更新,需要判断写操作是否来自回收线程,这里需要加上写屏障
  • 读屏障,通过新内存地址的brooks指针访问即可
  • 比较屏障,因为这两个对象实际是相等的,只是在不同的region,所以比较的时候要将老的 brooks指针,定位到新的brooks指针,然后重新比较

参考链接

参考