所有垃圾收集器一网打尽


前言

本文主要介绍市面上比较著名的垃圾收集器,其中也包括低延迟垃圾收集器。
其中最经典的七种,两两连线代表可以配合使用。
在这里插入图片描述


1.2.8.1 Serial收集器

在这里插入图片描述

在收集器工作时,必须STW,新生代用标记复制,老年代用标记整理。可能STW带来的印象不好,所以大部分人觉得这种串行收集器已经毫无用武之地,但是它是在单核少核状态小效率最高的收集器,因为不需要像其他并发收集器需要两次标记且不断维护记忆集的内存开销,或者是并行收集器需要开辟内存的开销,单核少核情况下也没那么多并发,所以STW的影响较小,专心致志做收集反而效率更高。它也是HotSpot客户端新生代默认的垃圾收集器

2. ParNew收集器

实质上是Serial收集器并行版本(这里注意,并行指的是多个GC线程并行而不是和用户线程一起并发或并行),其最大的特点是能够和CMS配合,在单核还不如Serial收集,因为多线程并行收集有线程交互的开销。

3. Parallel Scavenge收集器

该收集器和PartNew差不多,但是其聚焦于优化吞吐量(其他收集器更聚焦于优化STW停顿时间):

吞吐量=用户程序执行时间/(用户程序+垃圾收集程序执行时间)

该收集器可以通过参数的方式来设置每次垃圾收集时间的最大时间或者设置垃圾收集时间占总时间的占比。且该收集器还可以通过参数开启虚拟机自适应条件,根据不同的情况调节来保证最好的吞吐量

4.Serial Old

配合Parallel Scavenge收集器使用,作为CMS收集器失败的后备预案。
(Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非 直接调用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解,这里笔者也采用这种方式。)

5. Parallel Old

是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

6. CMS收集器

该收集器用于老年代,聚焦于缩小收集停顿的时间,基于标记清除算法实现。
分四步走:初次标记(STW),并发标记,重新标记(STW,时间比初次标记稍长,但比并发标记短得多),并发清除(因为采用标记清除,不需要移动存活对象,所以可以和用户线程并发)

缺点

1、并发阶段虽然可以和用户线程并发,但是占用比较多,只有当处理器核心数量超过4的时候才可以占用不超过25%的线程,用增量式并发收集器来解决这个状况,让用户和收集线程交互运行,不要出现一个线程独占一个资源很久的情况。直观感受是速度变慢的时间变长了,不是很好所以已经被废弃。
2、因为是和用户程序并发,用户线程执行过程中会不断产生垃圾,造成“浮动垃圾”,而CMS不能及时有效地处理这些垃圾,所以CMS不能等待老年代装满,而是到7成左右就要开始收集。但是现在已经将7成提升到9成,预留空间越来越小,就很有可能导致内存分配时空间不够,造成并发失败,这个是冻结所有用户进程,并采用Sercial Old进行垃圾收集。
3、因为是标记清除,所以产生很多内存碎片,解决方法是在经历很多次的不整理的FullGC,进行一次Full GC碎片整理

7.Garbage First收集器(G1)

因为CMS霸榜了很久,已经与虚拟机垃圾回收有着千丝万缕的关系,设计师将“行为”和“实现”分离,其他收集器都重构成接口的实现,为以后的更新换代做好准备。

G1原理

首先是该收集器不分老年代和新生代,把整个内存区域分为一个个内存块,看哪个内存块回收收益最大。
Humangous区域用来存储大对象,G1的大多数行为都将Humangous作为老年代来看待。
G1的记忆集采用双向卡表的结构,Key是别的Region的地址(即我指向谁),Value是卡表的索引号集合(即谁指向我)。但是该卡表实现方式的维护成本和内存成本都非常高,因为它是很多很多的Region的卡表,而不是像CMS那样子不需要考虑新生代对老年代的跨代引用(因为新生代朝生夕死,马上就变老年代了),只需要考虑老年代哪些内存块对新生代的跨代引用。
G1在每个Region中划分一部分内存用于在垃圾收集过程中的内存分配,设立两个指针TAMS,标记这段划分出来的内存,被分配到这个内存上的对象被G1隐式标记,即默认这些对象是存活的。
如果划分出来的内存不满足内存分配需要G1也会发生和CMS一样的“并发错误”
因为G1可以由用户指定停顿时常,好像越低的时常越能满足表面上停顿时间优化的目的,但是停顿时常一旦低到不合理时,每一次垃圾回收只能回收一小块内存,每过一段时间就需要一次FullGC来解决内存逐渐紧张的问题,反而降低了性能。
G1垃圾收集改变了业内想要一次性清空java堆的思想,而是转为高效率地一点一点地清理。
G1从整体上来看是采用标记整理,但是微观上来看是标记复制,不会产生内存碎片。

停顿时间模型

“停顿时间模型”:在某一段时间内,垃圾收集的时间不超过某个值(用户可以指定停顿时间)
如何建立良好的停顿时间模型:采用衰减均值,通过最近一段时间收集的时间来计算一些平均值什么的,但衰减均值与平均值最大的差异便是:平均值代表整体,衰减均值代表最近一段时间,即计算衰减均值的数据越新,越可靠,反之越不可靠。

G1垃圾收集器运行步骤

1、 初始标记:根节点枚举,设置TAMS,保证并发标记阶段和用户线程并行时有足够的内存供给用户线程进行分配。
2、 并发标记:
3、 最终标记:
4、 筛选回收:不是和用户线程并发执行的,可以收集几个回收价值大的Region组成回收集,把其中的存活对象复制到新的Region中,再把旧的全部清理掉。
在这里插入图片描述

8.低延迟垃圾收集器

衡量垃圾收集的指标:内存占用,吞吐量,延迟
随着硬件的发展,性能逐渐提高,程序执行越来越快,包括垃圾收集的程序,吞吐量毫无疑问也在逐渐升高。但是随着硬件容量扩大,比如收集1GB的垃圾肯定比收集1TB的垃圾产生更小的延迟(停顿时间也包含在延迟指标当中)。

8.1Shenandoah收集器

该收集器继承了G1的诸多特质,不同的地方是该收集器维护记忆集的方式是连接矩阵,即两个对象分别为M和N,那么在矩阵中第M和第N行打上一个标记。

工作顺序

1、 初始标记
2、 并发标记
3、 最终标记
4、 并发清理:这部分回收那些一个存活对象都没有的Region。
5、 并发回收:这一步收集器将所有存活对象统一复制到一个空闲的Region中,但是因为是和用户线程并发执行,用户还需要读写被移动的存活对象,这就很麻烦,使用转发指针“Brooks Pointer”来解决这个问题。
6、 初始引用更新:因为前面并发回收将存活对象统一复制移动了,地址改变了,所以更新所有旧的引用,但是这一步并没有作出实质的更新,只是建立了一个线程集合点,确保并发回收阶段都已经完成了所有复制任务。
7、 并发引用更新:真正的更新引用,要注意的是这一次不需要再像之前的收集器那样沿着图搜索,而是根据被复制到的地点的物理内存直接进行线性查找
8、 最终引用更新:之前都是解决了堆中对象与对象的引用更新,这一次主要更新GCRoot中的引用
9、 并发清理:收集非存活对象

Brooks Pointer(转发指针)

在并发回收的这个过程当中,用户程序访问到已经被复制后,但还存在的旧对象时,触发一种内存自险,并转入异常处理,把访问的地址转到新对象上。在没有进行并发时,这个指针指向自己,并发移动开始,指针指向新对象(当然也是复制过后)
与之前reference访问句柄后间接访问实例数据和类型数据的方式相似,句柄有一个统一的句柄池存放,但转发指针放在方法头中。
就算有转发指针,这也是线程不安全的,因为当收集器复制产生了新的对象,转发指针还没更新为新对象地址时,用户就往对象中写入数据,会出现问题,所以我们要保证写操作一定是要发生在新对象上,可以通过给收集线程访问旧对象加同步锁,在收集线程没完成之前,用户线程等待。但是Shenandoah收集器采用CAS操作。
因为转发指针的存在,程序需要执行的转发操作比较频繁,所有用户线程对旧对象的读写访问都需要转发,而这些都是用类似于AOP切面的读写屏障来实现的,因为转发的消耗较大,程序中的读操作远远大于写操作,所以该收集器被诟病的也是大量读屏障的设计。

8.2 ZGC收集器

动态Region

ZGC的Region区域是动态的,有大中小的分别。
小:2MB,只能放置小于256KB对象
中:32MB,只能放置大于256KB但小于4MB对象
大:2MB的整数倍,体现ZGC动态内存的地方,因为它有可能比中内存还小,根据对象的大小来决定大小。

并发整理算法(特点:染色指针)

根据GCRoot的可达性分析算法和追踪收集算法可以知道,我们在遍历这个GCRoot 图的时候,并没有遍历每个对象,而是遍历了指向这个对象的指针或者是标记。
对象被引用,就会有一个指针指向该对象的地址,虽然指针规定的寻址范围很大,比如一个指针可以寻2的9次方的地址,但是平时虚拟机根本没有这么大的寻址需求,所以指针空闲下来的位数就可以用于存储这个对象被复制和移动的信息(三色标记状态,是否被移动过,以及是否值得进行finalize方法)。这样,我们直接访问该指针,避免了用户线程和收集线程频繁竞争对象的访问权。
但是很多操作系统可不会管你的指针中是否还存在一部分是地址内容,一部分是其他信息,它们只会把整个指针的字节码当作是地址。
那么我们可以通过将包含:地址信息+不同其他信息的指针字节码映射到同一个物理地址当中,多对一的关系。

执行顺序

1、 并发标记:和G1等收集器一样,需要经过初次标记等等,但是ZGC扫描过程中,只需要访问指针中的数据,不需要访问对象头
2、 并发预备重分配:和G1将需要复制清理的Region组成一个回收集不同,ZGC会扫描整个Region,其形成的重分配集只能代表集合里的对象会被复制移动,而不是只从重分配集中扫描。每一次的全Region扫描虽然开上去消耗较大,但是其避免了记忆集的维护成本。
3、 并发重分配:
形成重分配集之后,更新指向这些对象的染色指针,让ZGC虚拟机一眼就能知道哪些存活对象需要被复制移动。
维护一个转发表,里面有旧对象和新对象的对应关系。当用户线程访问到旧对象时,被预先设置的内存屏障截获(避免了直接访问对象本身,读屏障可以大量减少使用,还可以在一个Region中的存活对象被复制完毕后立马清除并将其作为空闲的可分配内存的空间,因为只需要查转发表,不需要再去访问变为null的对象造成程序运行错误),内存屏障会让你去查转发表,成功访问新对象并把指向旧对象的指针修改为指向。这个过程称为指针的自愈,每个这样的查找只会经历一次,以后再找就方便了。
4、 并发重映射:主要内容是修复旧引用,并不是一个迫切的任务,因为当其他类通过旧引用访问这些被已经被复制的对象时,都需要经过一次指针的自愈,所以可以等到下一轮标记开始再慢慢地修正。

劣势

ZGC不分代,这就导致每次扫描都需要扫描整个内存,规避了记忆集的维护成本,但扫描时间过长,导致整个垃圾收集的效率会降低。如果系统在这段时间猛分配内存,收集速度赶不上分配速度就会出现问题(分代的话,收集器会花大部分精力去收集新生代的垃圾,老年代偶尔看一下,所以手机效率很高)

支持NUMA-Aware分配

因为现在大部分都是多核处理器,每个处理器都有自己管理的内存,跨核访问会很慢,ZGC和Parallel Scanvenge支持Numa,可以优先在处理器自己的内存下分配对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值