图中展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器亦或是老年代收集器。
名词解释
- 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
- 并发(Concurrent): 并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
- 吞吐量(Throughput):处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:
- 浮动垃圾(Floating Garbage):并发标记和并发清理阶段,用户线程是还在继续运行的,会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉他们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
- 并发失败(Concurrent Mode Failure):垃圾收集器运行期间,预留的内存无法满足程序分配新对象的出现,就会出现一次“Concurrent Mode Failure”。
- 增量更新(Incremental Update):详见垃圾回收算法。
- 原始快照(Snapshot At The Beginning(SATB)):详见垃圾回收算法。
Serial收集器
Serial收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(“Stop The World”)。
原理
特性
迄今为止,它依然是HopSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比)。对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一 百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
原理
特性
ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
Parallel Scavenge收集器
Parallel Scavenge收集器的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。
原理
使用标记-清除算法,并行收集的多线程收集器。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis
参数以及直接设置吞吐量大小的-XX:GCTimeRatio
参数。还有一个开关参数-XX:+UseAdaptiveSizePolicy
。
-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收话费的时间不超过用户设定值。垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-XX:GCTimeRatio参数的值是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
-XX:+UseAdaptiveSizePolicy参数被激活后,就不需要人工置顶新生代的大小-Xmn
、Eden与Survivor区的比例-XX:SurvivorRatio
、晋升老年代对象大小-XX:PretenureSizeThreshold
等细节参数了,虚拟机会根基当前系统的运行情况手机性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。这种调节方式成为垃圾收集的自适应的调节策略(GC Ergonomics)。
特性
Serial Old收集器
Serial Old是Serial收集器的老年代版本。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
原理
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
在它出现之前(JDK 6之前),新生代的Parallel Scavenge收集器只能搭配Serial Old(PS MarkSweep)使用,老年代Serial Old收集器在服务端应用性能不高,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合。
原理
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。
原理
CMS收集器是基于标记-清除算法实现的,整个过程分为四步:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(详见关于增量更新的讲解),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
特性
优点
缺点
- 由于是并发的,所以对处理器资源非常敏感。CMS默认启动的回收线程数是是
(处理器核心数量+3)/4
,当处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并随着处理器核心数量的增加而下降。但当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。 - 无法处理浮动垃圾,且垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能等待老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用要是CMS运行期间预留的内存无法满足程序分配新对象的需要,有会出现“Concurrent Mode Failure”并发失败,虚拟机将冻结用户进程的执行,临时启用Serial Old收集器重新进行老年代的垃圾收集,导致了完全“Stop The World”的Full GC的产生,停顿时间就很长了。相关参数:
-XX:CMSInitiatingOccu-pancyFraction
(CMS的触发百分比),JDK 5默认是68%,JDK 6默认是92%。
- 由于基于“标记-清除”算法实现,收集结束时会有大量空间碎片产生,大对象分配时,如果无法找到足够大的连续空间来分配当前对象,就不得不提前触发一次Full GC。相关参数:
-XX:+UseCMSCompactAtFullCollection
开关参数,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程。-XX:CMSFullGCsBeforeCompaction
(从JDK 9开始废弃)CMS收集器在执行过若干次(参数指定的值)不整理空间的Full GC后,下一次进入Full GC前会先进行碎片整理,默认值为0。Garbage First(G1)收集器
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
作为CMS收集器的替代者和继承人,设计者们希望做出一款能够建立起“停顿时间模型”(Pause Prediction Model)的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特征了。原理
基本回收策略
G1可以面向堆内存任何部分来组成回收集(Collection Set)进行回收,衡量标准不再是它属于哪个分代,而是那块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间,Region是单次回收的最小单元。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize
设定,取值范围为1MB~32MB,且应为2的N次幂。而对于哪些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
G1的新生代和老年代不再是固定的了,他们都是一系列区域(不需要连续)的动态集合。G1收集器会跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(参数-XX:MaxGCPauseMillis
,默认是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来,这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
跨Region引用问题
为了解决跨Region引用对象可能会导致全堆作为GC Roots的问题,G1也使用了记忆集。G1收集器的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显多得多,因此G1收集器和传统垃圾收集器相比有更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。保证收集线程与用户线程互不干扰
G1收集器通过原始快照(SATB)算法保证了用户线程改变对象引用关系时,不会打破原本的对象图结构。由于程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。同样,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。建立可靠的停顿预测模型
用户通过-XX:MaxGCPauseMillis
参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。运作过程
如果不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),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的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
特性
优点(相对于CMS)
G1从整体上来看是基于“标记-整理”算法实现的,但从局部(两个Region之间)看又是基于“标记-复制算法”实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存,有利于程序长时间运行。
缺点(相对于CMS)
为了垃圾收集产生的内存占用(Footprint)高。G1的卡表实现比CMS更为复杂,而且堆中每个Region都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间。
程序运行时的额外执行负载(Overload)高。G1除了使用写后屏障来进行卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。
注意点
收集停顿时间-XX:MaxGCPauseMillis
默认为200毫秒,不能设置得过低,否则可能会因为停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积,最终占满堆引发Full GC反而降低性能,所以通常把停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。