《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。
上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
收集器可划分为并行和并发两种。
并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
Serial收集器
1)简要介绍
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。
2)收集模式
这个收集器是一个单线程工作的收集器,在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
3)运行示意图
上图为Serial(新生代)/Serial Old(老年代)收集器运行图。
4)优缺点
简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;
对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
“Stop The World”的用户体验较差。但垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。
所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
5)使用
HotSpot虚拟机运行在客户端模式下的默认新生代收集器。
ParNew收集器
1)简要介绍
ParNew收集器实质上是Serial收集器的多线程并行版本。
除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop TheWorld、对象分配规则、回收策略等都与Serial收集器完全一致。
2)运行示意图
上图为ParNew(新生代)/Serial Old(老年代)收集器运行图。
3)优缺点
ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越Serial收集器。
随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,在处理器核心非常多(譬如32个,现在CPU都是多核加超线程设计,服务器达到或超过32个逻辑核心的情况非常普遍)的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
4) 使用
ParNew收集器是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器(下文会有详细介绍)配合工作。
随着垃圾收集器技术的不断改进,更先进的G1收集器带着CMS继承者和替代者的光环登场。G1是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作。
所以,自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代。
Parallel Scavenge收集器
1)简要介绍
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
2)收集模式
Parallel Scavenge收集器与其他收集器关注点不同。
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
所以也被称为“吞吐量优先收集器”。
吞吐量,是指处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:
如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
3)控制参数
控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数,它允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。
直接设置吞吐量大小的-XX:GCTimeRatio参数,它的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
4)优点
-XX:+UseAdaptiveSizePolicy,这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数。
虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。
如果我们对收集器运作不太了解,手工优化存在困难的话,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。
我们只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(更关注最大停顿时间)或-XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。
自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。
Serial Old收集器
1)简要介绍
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
2)使用
主要意义是供客户端模式下的HotSpot虚拟机使用。
如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent ModeFailure时使用。
Parallel Old收集器
1)简要介绍
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
2)运行示意图
上图为Parallel Scavenge(新生代)/Parallel Old(老年代)收集器运行图
3)使用
这个收集器是直到JDK 6时才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
CMS收集器
1)简要介绍
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,也成为“并发低停顿收集器”。
2)收集模式
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
初始标记(CMS initial mark):标记一下GC Roots能直接关联到的对象,速度很快;会发生“Stop The World”。
并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
重新标记(CMS remark):为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
3)运行示意图
上图为Concurrent Mark Sweep收集器运行图
4)优缺点
优点:
并发收集、低停顿。
缺点:
当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活;到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。
这面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
G1收集器
1)简要介绍
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
JDK9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。
2)模式对比
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。
而G1可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region中一类特殊的区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
3)收集模式
G1收集器将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
G1收集器会跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。
这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
4)运行示意图
初始标记(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的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成。
5)对比CMS收集器
优势:
G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。
这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。
劣势:
就内存占用来说,虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间。
相比起来CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。