Java垃圾收集器

220 篇文章 1 订阅
43 篇文章 2 订阅

今天就来详细讲讲HotSpot经典的几款垃圾收集器。

一、各款经典收集器之间的关系图

图片

1、图片展示了各种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用(这个关系不是一成不变的,由于维护和兼容性测试的成本,在JDK 8时将 Serial + CMS、ParNew + Serial Old这两个组合声明为废弃,并在JDK 9中完全取消了这些组合的支持),图中收集器所处的区域,则表示它是属于新生代收集器或是老年代收集器。

2、在介绍这些收集器各自的特性之前,需要明确一个观点:虽然会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。

这点不需要多加论述就能证明:如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,HotSpot虚拟机完全没必要实现那么多种不同的收集器了

二、Serial收集器

1、Serial收集器运行示意图

图片

2、Serial收集器介绍

(1)Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。

(2)大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器,它在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

(3)“Stop The World”这个词语也许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。读者不妨试想一下,要是你的电脑每运行一个小时就会暂停响应五分钟,你会有什么样的心情?

3、Serial收集器适用场景

(1)迄今为止,Serial收集器依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

(2)在用户桌面的应用场景中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。

(3)所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择

三、ParNew收集器

1、ParNew收集器运行示意图

图片

2、ParNew收集器介绍

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:

-XX:SurvivorRatio、

-XX:PretenureSizeThreshold、

-XX:HandlePromotionFailure等)、收集算法、STW、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。

3、ParNew收集器适用场景

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

四、Parallel Scavenge收集器

1、 Parallel Scavenge收集器运行示意图

图片

2、Parallel Scavenge收集器介绍

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器……Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,那它有什么特别之处呢?

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值

3、Parallel Scavenge收集器适用场景

如果对于收集器运作不太了解,手工优化存在困难的话,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。

只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(更关注最大停顿时间)或-XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。

自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。其实ParNew的Par就是Parallel的简写,ParNew就是Parallel Scavenge的增强版,为了配合CMS ParNew做了一些增强。

五、Serial Old收集器

1、Serial Old收集器运行示意图(同Serial示意图)

图片

2、Serial Old收集器介绍

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

3、Serial Old收集器适用场景

Serial Old收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

如果在服务端模式下,它也可能有两种用途:

(1)一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,

(2)另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。

六、Parallel Old收集器

1、Parallel Old收集器运行示意图(同Parallel Scavenge)

图片

2、Parallel Old收集器介绍

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

这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。

由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比ParNew加CMS的组合来得优秀。

3、Parallel Old收集器适用场景

在Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

七、CMS收集器

1、CMS收集器运行示意图

图片

2、CMS收集器介绍

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统STW尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些。

整个过程分为四个步骤,包括:
1)初始标记(CMS initial mark)

有STW;初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快。

2)并发标记(CMS concurrent mark)

并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与GC线程一起并发运行。

3)重新标记(CMS remark)

有STW;重新标记阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;采用三色标记算法和增量更新避免漏标。

4)并发清除(CMS concurrent sweep)

清理删除掉标记阶段判断的已经死亡的 对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发执行的。

3、CMS收集器的缺点

(1)CMS收集器对处理器资源非常敏感。

(2)由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全STW的Full GC的产生,即让Serial Old来进行收集。浮动垃圾就是并发标记、并发清理时产生的垃圾。

(3)大量空间碎片的产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

八、Garbage First收集器

1、G1收集器分区示意图

图片

E:Eden区

S:Survivor区

H:Humongous区(存放大对象)

2、G1运行示意图

图片

3、G1收集器介绍

在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

G1开创的基于Region的堆内存布局是它能够实现垃圾优先回收目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与G1之前的收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。

4、G1收集器缺点

G1无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比CMS高

5、G1收集器适用场景

目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,当然,这也不是绝对的,不同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也会让对比结果继续向G1倾斜。

总的来说,HotSpot的垃圾收集器是伴随着内存发展而不断前进的,早期几十M的内存,Serial+Serial Old单线程进行回收就足以;但是内存达到几百M时,就得使用PS+PO多线程的GC线程来回收;当内存达到几个G时,多线程也忙不过来,就得使用并发的CMS+ParNew收集器;当到了动辄几十个G内存的时候,以前那种每次GC都进行新生代或老年代或整个堆的回收的STW也无法忍受时,就得使用G1了

目前绝大数的生产环境都是使用的JDK8,若没有进行过调优,默认使用的是PS+PO的收集器;但是要进行调优时,会在CMS和G1中来进行选择,如果内存比较大(10G以上),最好使用G1,内存较小(几个G)可以考虑CMS

但是不管怎样,正如文章开始所说的,如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,HotSpot虚拟机完全没必要实现那么多种不同的收集器了

只有最合适的收集器,没有最好的收集器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值