JVM有哪些垃圾收集器

789 阅读11分钟

Serial收集器

Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代 收集器的唯一选择。 大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强 调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”这个词语也 许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况 下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。读者不妨试想一下,要是 你的电脑每运行一个小时就会暂停响应五分钟,你会有什么样的心情?下图示意了Serial/Serial Old收集器的运行过程。

Image.png

Image [2].png 每次回收时只有一个线程,因此串行回收器在并发能力较弱的计算机上,其专注性和独占性的特点往往能让其有更好的性能表现。 之所以需要“Stop the world"是因为不希望在进行垃圾收集的时候,又产生新的垃圾导致回收不干净。有一个很好的例子: 比如你在清理垃圾的时候,一边清扫一边丢垃圾肯定永远都扫不干净,所以需要规定打扫的时候不能扔垃圾,还比如,去公共卫生间时,经常会遇到阿姨在里面打扫垃圾,在外面挂一个牌子——保洁中,暂停使用。 这就是简单粗暴地Stop the world,虽然简单粗暴,但是效率也很高,如果上述的打扫只需要几分钟或者阿姨保洁只需要几分钟,我们也是完全可以接受的。JVM的垃圾收集与此有一定的相似之处,但是又远比这些简单的工作要复杂得多。虽然暂停用户线程的时间一直在优化而减少,但是Serial依然因为其“Stop The World”收到诟病。 但事实上,迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内 存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理 器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以 获得最高的单线程收集效率。在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚 拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的 内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一 百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。 总结:Serial是一个单线程的垃圾收集器,适用于桌面客户端应用,其新生代回收采用复制算法,老年代回收采用标记整理法。

ParNew收集器

因为Serial在GC的时候是单线程的,而我们又要尽量地缩短STW(Stop The World)的时间,因此在进行GC的时候可以采用多线程并发的方式进行,而ParNew收集器实质上就是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规 则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。ParNew收 集器的工作过程如下所示

Image [3].png

Image [4].png

ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它 却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。

总结:ParNew收集器是Serial的并行版本,它是新生代收集器,依然采用复制算法对新生代进行收集,老年代可搭配Serial Old进行收集,因为它能够搭配CMS收集器,ParNew收集器才成为服务端默认的新生代垃圾收集器。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是 能够并行收集的多线程收集器……Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,那它有 什么特别之处呢? Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能 地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值

Image [6].png

Image [5].png Parallel Scavenge收集器设计的目的是让开发者能够让开发者可以控制垃圾收集器的吞吐量。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。 总结:Prallel Scavenge也是一款新生代垃圾收集器,老年代可以搭配Serial Old和Parallel Old进行回收,其特点是可以控制最大垃圾收集停顿时机和吞吐量

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。 总结:Serial收集器的老年代版本,使用标记-整理算法

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实 现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。 Parallel Old收集器的工作过程如图:

Image [7].png 直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组 合。 总结:Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为 关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非 常符合这类应用的需求。 从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作 过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括: 1)初始标记(CMS initial mark) 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。 2)并发标记(CMS concurrent mark) 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;并发标记的时间可能长达整个垃圾收集过程的70%以上。 3)重新标记(CMS remark) 重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。 4)并发清除(CMS concurrent sweep) 并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。 由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的,整个过程如下图:

image.png CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来: 并发收集、低停顿,一些官 方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:

  1. 虽然不会导致用户线程停顿,但是会增加GC线程,因此他会占用一部分线程资源而导致用户线程变慢,降低吞吐量。
  2. CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
  3. CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。(老年代的收集器大都采用标记整理算法,就是为了减少产生空间碎片,因为老年代有很多的大对象需要连续的空间来存储

CMS相关的参数:

参数类型默认值备注
-XX:+UseConcMarkSweepGCbooleanfalse启用CMS,老年代采用CMS收集器收集
-XX:+CMSScavengeBeforeRemarkbooleanfalse在重新标记前先执行一次Minor GC
–XX:-UseCMSCompactAtFullCollectionbooleanfalse对老年代进行压缩,可以消除碎片,但是可能会带来性能消耗
-XX:CMSFullGCsBeforeCompaction=nuintx0CMS进行n次full gc后进行一次压缩。如果n=0,每次full gc后都会进行碎片压缩。如果n=0,每次full gc后都会进行碎片压缩
–XX:+CMSIncrementalModebooleanfalse并发收集递增进行,周期性把cpu资源让给正在运行的应用
–XX:+CMSIncrementalPacingbooleanfalse根据应用程序的行为自动调整每次执行的垃圾回收任务的数量
–XX:ParallelGCThreads=nuintx并发回收线程数量:(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8)
-XX:CMSInitiatingOccupancyFractio=nuintxjdk5 默认是68% jdk6默认92%当老年代内存使用达到n%,开始回收
注意:有一些参数是联动的,比如设置了UseCMSCompactAtFullCollection之后,对老年代进行压缩的参数CMSFullGCsBeforeCompaction才会生效

Garbage First(G1)收集器

G1垃圾收集器的内容比较多,实现比较复杂因此放到下一篇笔记了

【参考】
【1】《深入理解JAVA虚拟机》
【2】bilibili的UP主子烁爱学习

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。