1.前言
在前面我们已经说过了垃圾收集算法,那么现在我们要讲的垃圾收集器,实际上就是对垃圾收集算法的实践。
首先我们先看一张图,这张图可以帮助我们了解各款经典垃圾收集器之间的关系:
图中的垃圾收集器所在的区域代表了它是属于新生代或是老年代的收集器。
在正式了解垃圾收集器之前,我们务必要明确一个观点:
虽然垃圾收集器的技术在不断进步,但是直到现在也没有最好的垃圾收集器出现,更不存在万能的收集器,所以我们选择的只是对具体应用最合适的收集器。
2.垃圾收集器
2.1 Serial收集器
概要
Serial收集器是最基础、历史最悠久的收集器,这个收集器是一个单线程工作的收集器,单线程的意义并不是说它只会用一个处理器或者一条收集线程区完成垃圾收集工作,更重要的是强调它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
运行过程
用法
虽然Serial是单线程进行垃圾收集,但是它也有自己的优势所在。迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,相较于其他收集器,他最大的优势就是简单而高效(与其他收集器的单线程相比)。
- 对于资源受限的环境下,它是所有垃圾收集器里额外内存消耗最小的
- 对于单核处理器或者处理器核心数较少的情况下,Serial收集器由于没有线程交互的开销,自然可以获得最高的单线程收集效率
在近年来流行的微服务应用中,分配虚拟机的内存一般不会特别大,收集几十兆甚至一两百兆的新生代,垃圾收集的停顿时间完全可以控制在十几、几十毫秒,只要不频繁发生垃圾收集,这点停顿时间对用户来说完全可以接受。利用这个优势,所以Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
2.2 ParNew收集器
概要
ParNew收集器实质上是Serial收集器的多线程并发版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX: SurvivorRatio
、-XX: PretrnureSizeThreshold
、-XX: HandlePromotionFailure
等)、收集算法、Stop the World、对象分配规则、回收策略都与Serial收集器完全一致。
运行过程
用法
ParNew除了支持多线程以外,相对于Serial收集器并无太多创新,但是它却是不少运行在服务端模式下的HotSpot虚拟机。
很重要的原因是:除了Serial收集器外,只有它能与CMS收集器配合工作。
CMS作为老年代的收集器,无法与新生代中的Parallel Scavenge配合工作,所以在使用CMS收集器时,新生代只能选择Serial或者ParNew收集器。
正是CMS收集器出现巩固了ParNew的地位,JDK9直到G1出现,ParNew+CMS不再是官方推荐的服务器端的收集器解决方案了。从JDK9开始ParNew正式并入CMS,只能两者搭配适用。ParNew是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。
ParNew收集器在单核心处理器中绝对不会有比Serial更好的效果。
2.3 Parallel Scavenge收集器
概要
Parallel Scavenge收集器是一款新生代收集器,它同样基于标记-复制算法实现,也是能够并行收集的垃圾收集器。
Parallel Scavenge收集器的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程等待的时间,而Parallel Scavenge的目标是达到一个可控制的吞吐量。
吞吐量 = 运行用户代码时间 / (运行用户时间代码 + 运行垃圾收集时间)
除此之外,Parallel Scavenge还提供了自适应调节策略!这也是该收集器的重要特点之一!
运行过程
用法
高吞吐量可以最高效率利用处理器资源,尽快完成程序的运算任务,主要适合后台运算不需要太多交互的分析任务。
Parallel Scavenge提供了两个参数控制吞吐量:
- 控制最大垃圾收集停顿时间:-XX: MaxGCpauseMillis
- 改参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收所花费的时间不超过用户设定值。
- 注意:垃圾收集停顿时间缩短是牺牲吞吐量和新生代空间为代价换取的!不能简单的认为将改参数设置的小一点就可以使垃圾收集的速度变快。
- 直接设置吞吐量大小:-XX: GCTimeRatio
- 这个值应该设置为一个大于0小于100的毫秒数,也是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
- 举个例子,我们将该参数设置为19,那么允许的最大垃圾收集时间占总时间就是5%(即1/(1+19) )。默认是99。
除此之外,Parallel Scavenge还提供了一个参数用于自适应调节策略:
- 自适应调节策略:-XX: +UseAdaptiveSizePolicy
- 当这个参数开启以后,就不需要指定新生代的大小、Eden和Survivor区的比例、晋升老年代对象的大小等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
如果我们对收集器的运作不太了解,手动优化存在困难的话,适用该自适应调节策略也是一个不错的选择。只需要设置最大堆内存,然后选择优化目标(停顿时间/吞吐量大小),具体的细节就有虚拟机自己来完成了。
2.4 Serial Old收集器
概述
Serial Old是Serial收集器的老年代版本。它同样是一个单线程收集器,适用标记-整理
算法。
运行过程
参考Serial收集器中的图。
用法
该收集器主要的意义也是供客户端模式下的HotSpot虚拟机上使用。
如果用在服务端模式下,可能有两种用途:
- 在JDK5版本之前搭配Parallel Scavenge收集器使用
- 作为CMS收集器失败时的后备预案,在并发收集发生
Concurrent Mode Failure
时使用
2.5 Parallel Old收集器
概述
Parallel Old是Parallel Scavenge收集器的老年代版本,同样支持多线程并发收集,基于标记-整理
算法实现。
运行过程
同样见Parallel Scavenge收集器中的图。
用法
在注重吞吐量或者处理器资源比较稀缺的情况下,可以优先考虑Parallel Scavenge + Parallel Old
收集器这个组合。
2.6 CMS收集器
概要
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。其基于标记-清除
算法实现。
运行过程
CMS垃圾收集一共可以分为四个步骤:
- 初始标记:仅仅标记一下GC Roots能够直接关联到的对象,速度很快
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以同垃圾收集线程一起并发运行
- 重新标记:修正并发期间,因用户程序继续运作导致标记产生变动的那一部分对象标记记录,这个阶段的停顿时长比初始标记要长,但是远比并发标记的时间要短
- 并发清除:清理删除掉标记阶段判断已经死亡的对象,由于不需要移动存活对象,所以该阶段也可以与用户线程并发运行
以上四个步骤,初始标记与重新标记仍然需要Stop The World
。
用法
目前很大一部分Java应用集中在互联网网站或者基于浏览器的B/S系统的服务器上,这类应用通常较为关注服务的响应速度,希望停顿时间尽可能短,给用户带来良好体验。CMS收集器就非常符合这类应用的需求。
缺点
CMS是一款优秀的垃圾收集器,是HotSpot虚拟机追求低停顿第一次成功尝试。
不过他还远达不到完美的地步,CMS至少有以下三个明显的缺点:
- CMS收集器对处理器资源非常敏感
- 在并发阶段,虽然不会导致用户线程停顿,但是由于占用了一部分CPU资源,会导致应用程序变慢,降低吞吐量。
- CMS默认启动的回收线程是
(处理器核心+3)/4
个,也就是说如果处理器核心在4个或以上,并发回收时垃圾处理线程只占用不到25%的处理器资源,并且会随着处理器核心的增加而下降。但是当处理器核心不足4个的时候,CMS对用户的影响就可能变得很大。
- CMS收集器无法处理浮动垃圾,有可能出现
Con-current Mode Failure
失败进而导致另一次完全Stop The World
的Full GC的产生。- 浮动垃圾:指在CMS并发标记和并发清理阶段,用户线程还是在继续运行的,程序在运行自然还会伴随新的垃圾对象不断产生,但是这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理它们,只好留待下一次收集时再清理掉。这一部分垃圾就叫做浮动垃圾。
- 由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
- 在JDK 5的默认设置下,CM S收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果 在实际应用中老年代增长并不是太快,可以适当调高参数
-XX:CMSInitiatingOccu-pancyFraction
的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。 - 到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次并发失败(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。
- 在JDK 5的默认设置下,CM S收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果 在实际应用中老年代增长并不是太快,可以适当调高参数
- 由于CMS收集器基于标记清除算法实现,意味着收集结束时会有大量的空间碎片产生
2.7 Garbage First收集器
概要
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局模式。
G1是一款“停顿时间模型”的垃圾收集器。
所谓停顿时间模型的意思就是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不会超过N毫秒这样的目标。
G1之所以能够够做到停顿时间模型主要有两个原因:
-
G1开创的基于Region的堆内存布局是他能够实现这个目标的关键。
-
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
-
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数
-XX:G1HeapRegionSize
设定,取值范围为 1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待,如下图(G1收集器Region分区示意图)所示: -
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rzvY16b6-1683418863157)(深入理解Java虚拟机——垃圾收集器.assets/d5a2385dd9eb725b93c5d4edf37b7fc6.png)]
-
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。
-
-
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
- 更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数
-XX:MaxGCPauseMillis
指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。
- 更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数
这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
运行过程
G1收集器的运作过程大致可划分为以下四个步骤:
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1收集器除了并发标记外,其余阶段都是需要暂停用户线程的。换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。
用法
JDK 8 Update 40 版本之后,G1收集器被Oracle官方称为“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
G1是一款主要面向服务端应用的垃圾收集器。JDK 9之后,G1取代了Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。
3.拓展知识
关于G1与CMS的经验之谈
目前在小内存应用上CMS的表现大概率要会优于G1,而在大内存应用上G1则大多能发挥其 优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。不同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也 会让对比结果继续向G1倾斜。
本文参考自《深入理解Java虚拟机》(第三版)