深入理解jvm虚拟机-常用垃圾回收器解读
概览
CMS(Concurrent Mark Sweep)垃圾回收器
- 采用标记清除算法
- 分为四阶段
- 初始标记,用户线程需要停顿
- 并发标记,用户线程不需要停顿
- 重新标记,使用增量更新,用户线程不需要停顿
- 并发清除,用户线程不需要停顿
- 缺点:
- 占用资源过高,降低程序吞吐量
- 浮动垃圾无法清理,需要等到下一次再处理,这就导致了cms不能等到老年代快要占满了才去进行垃圾回收,得需要预留一些空间给浮动垃圾,如果在并发清理期间无法分配内存至新的对象,那么会导致并发失败,此时虚拟机启用serial old垃圾回收进行处理
- 内存碎片过多
Garbage First垃圾回收器
面向局部回收的收集思路和基于Region的内存布局形式,在JDK9中,G1取代了Parallel Scavenge组合,G1的开创基于region的堆内存布局是他能够实现这个目标的关键,G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分成为多个大小相等的独立区域,每一个region都可以根据需要,扮演新生代的Eden区、Survivor区、或者老年代空间,收集器可以根据扮演不同角色的region采用不同的策略去处理,每个region的大小是2的N次幂,在region中还有一类特殊的humongous区域,专门用来存储大对象,只要对象超过region的一半区域就是大对象。
G1核心思想是我停顿一定的时间内回收的垃圾要最多,尽可能大的提高垃圾回收的效率,这也是Garbage First的由来。
它使用原始快照的方式来解决引用关系发生改变的情况
四阶段:
- 初始标记,需要停顿用户线程
- 并发标记,不需要停顿用户线程
- 最终标记,需要停顿用户线程,使用原始快照的方式去处理第二步中出现的引用关系改变的问题
- 筛选回收,需要停顿用户线程,宏观上使用标记-整理算法,将需要回收的region中的存活对象复制整理到空的region,然后清空旧的region,但是复制的这个行为又像是标记复制的算法
和cms相比的优劣势
- g1在收集过程中不会产生内存碎片,但是cms会
- cms使用写后屏障维护卡表,且卡表结构简单,但是g1卡表复杂,写前屏障保存原始快照,写后屏障更新卡表,占用资源较多
- cms写屏障是同步操作,g1的写屏障是异步操作
- 在小内存收集上cms会占优势一点,小内存大概是6-8GB
Shenandoah垃圾回收器
概述
基于G1的升级版本,目标是降低停顿延迟,在筛选回收阶段实现了用户线程不停顿并进行整理内存,并且换掉了G1中复杂卡表的数据结构,采用连接矩阵来记录跨代引用的问题,并且彻底不会将region进行分代,从发展历程上来讲,它更像是G1的加强版:
- 跟G1相比,它不适用默认的分代收集
- 跟G1相比,在并发回收阶段,它可以支持和用户线程进行并发执行
- 跟G1相比,更换了卡表的数据结构,采用了更简单的连接矩阵来解决跨Region引用的状态,连接矩阵类似于图的邻接矩阵,将每个region抽象成为图的一个顶点,如果两个顶点之间存在跨代引用,则在矩阵中进行标记
垃圾回收详细过程
在垃圾回收阶段,Shenandoah垃圾回收器共分以下几个阶段:
- 初始标记,与G1一样,首先标记GC Roots,需要停顿用户线程
- 并发标记,与G1一样,从GC Roots遍历对象图,不需要停顿用户线程
- 最终标记,与G1一样,处理在并发标记过程中引用关系发生改变的情况,使用原始快照算法,需要停顿用户线程
- 并发清理,用于清理整个区域没有一个存活对象的region
- 并发回收,这是Shenandoah垃圾回收器与其他垃圾回收不同的一点,在这一步中,Shenandoah做到了在回收阶段能够与用户线程一起并发的程度,使用了读屏障和转发指针的技术(Brooks Pointer)
- 初始引用更新,在这一步中,垃圾回收并不会做一些实质性的工作,而是为了创建一个时间点,来保证所有垃圾回收线程能够做完对象复制的操作,用户线程需要做短暂停顿
- 并发引用更新,更新新对象内存引用的改变,与用户线程一起并发
- 最终引用更新,更新GC Roots,用户线程需要做短暂停顿
- 并发清理,清理那些没有存活对象的Region
读屏障与转发指针
实际上Shenandoah垃圾回收器对于其他垃圾回收器最大的不同是在垃圾回收阶段能够做到用户线程不停顿,与其一起并发的程度,但为了实现这个目标,需要解决核心问题是,在复制过程中,如果让用户线程能够使用和更新最新版本的对象?常规解决思路是通过内存保护陷阱去解决,当用户去访问到原先的内存时发生中断,但这样会导致操作系统用户态和核心态的频繁转换,降低性能,Shenandoah提出了新的解决方法,在对象头中增加一个指针位来标识对象的指针,如果对象没有被复制,那么转发指针指向自己,如果被复制,则指向新的复制对象:
所以在修改brooks pointer的时候需要进行同步操作才能保证用户线程和垃圾回收线程的一致性,在Shenandoah中采用的是cas自旋锁的操作,当其修改brooks pointer时会进行加锁,保证brooks pointer时刻指向新的对象,同时为保证用户线程在读取对象之前要走brooks pointer,Shenandoah使用了读屏障技术,走了一层转发。
ZGC
ZGC是一款基于Region内存布局,不设分代,使用了读屏障、染色指针和内存多重映射技术的来实现可并发的标记-整理算法,以低延迟为首要目标的一款垃圾回收器
特点
- Region不再是固定的,而是可以动态创建和销毁,拥有动态的区域大小,分为小中大三种:
- 小型Region:2MB
- 中型Region:32MB
- 大型Region:容量不固定的,可以动态变化,但必须为2MB的整倍数,用于放置4MB或以上的大对象,每个大型Region只会存放一个大对象
- 使用染色指针来解决在回收阶段与用户线程并发的问题
垃圾回收阶段
- 并发标记,与G1、Shenandoah一致,只不过是在对象的指针上做文章
- 并发预备重分配,根据特定的查询条件统计得出本次收集过程中要清理哪些Region,这些Region的存活对象都要被复制到其他Region中去
- 并发重分配,重分配是ZGC执行过程中的核心阶段,这个过程要把第二步的对象复制到新的Region中,,并为每个对象维护一个转发表,记录从旧对象到新对象的转向关系,得益于染色指针的支持,ZGC收集器可以就从引用上得知一个对象是否处于重分配集之中,如果用户线程此刻并发访问了位于重分配集的对象,这次访问会被预置的内存屏障所获取,然后根据转发表转发到新对象,并且修正更新引用的值,这种行为叫做指针的自愈
- 并发重映射,修正整个堆中指向重分配集中旧对象的所有引用,实际上去释放第三步中维护的转发表,会合并到下次垃圾回收的并发标记阶段中执行