1. 垃圾收集器的概念
定义 ;如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。《Java虚拟机规 范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含 的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特 点和要求组合出各个内存分代所使用的收集器。本文标题中“ 经典 ” 二字并非情怀,它其实是讨论范围的限定语,这里讨论的是在 JDK 7 Update 4 之后(在这个版本中正式提供了商用的G1 收集器,此前 G1 仍处于实验状态)、 JDK 11 正式发布之前,OracleJDK 中的 HotSpot 虚拟机 所包含的全部可用的垃圾收集器。使用“ 经典 ” 二字是为了与几款目前仍处于实验状态,但执行效果上有革命性改进的高性能低延迟收集器区分开来,这些经典的收集器尽管已经算不上是最先进的技术,但它们曾在实践中千锤百炼,足够成熟,基本上可认为是现在到未来两、三年内,能够在商用生产环境上放心使用的全部垃圾收集器了。各款经典收集器之间的关系用下图表示:图中 展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用 ,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。接下来本文将逐一介绍这些收集器的目标、特性、原理和使用场景,并重点分析CMS 和 G1 这两款相对复杂而又广泛 使用的收集器,深入了解它们的部分运作细节。值得注意的是:
虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有 最好的收集器出现,更加不存在“ 万能 ” 的收集器,所以我们选择的只是对具体应用最合适的收集器。
2. Serial收集器
工作图:定义:Serial收集器是最基础、历史最悠久的收集器,曾经(在 JDK 1.3.1 之前)是 HotSpot 虚拟机新生代 收集器的唯一选择。大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器。但它的“单线程” 的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须 暂停其他所有工作线程 ,直到它收集结束。“Stop The World”这个词语也 许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况 下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。大家不妨试想一下,要是你的电脑每运行一个小时就会暂停响应五分钟,你会有什么样的心情?早期 :对于“Stop The World” 带给用户的恶劣体验,早期 HotSpot 虚拟机的设计者们表示完全理解,但也 同时表示非常委屈:“ 你妈妈在给你打扫房间的时候,肯定也会让你老老实实地在椅子上或者房间外待 着,如果她一边打扫,你一边乱扔纸屑,这房间还能打扫完?” 这确实是一个合情合理的矛盾,虽然垃圾收集这项工作听起来和打扫房间属于一个工种,但实际上肯定还要比打扫房间复杂得多优点:1. 简单而高效迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比)。
对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;
对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的
内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一 百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial 收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。
3.ParNew收集器
工作图:
定义:
ParNew收集器实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial 收集器可用的所有控制参数(例如: -XX : SurvivorRatio 、 -XX :PretenureSizeThreshold、 -XX : HandlePromotionFailure 等)、收集算法、 Stop The World 、对象分配规 则、回收策略等都与Serial 收集器完全一致,在实现上这两种收集器也共用了相当多的代码。优缺点:
ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
在JDK 5 发布时, HotSpot 推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器 ——CMS收集器。这款收集器是 HotSpot 虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让 垃圾收集线程与用户线程(基本上)同时工作 。遗憾的是, CMS作为老年代的收集器 ,却 无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作 ,所以在 JDK 5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial收集器中的一个。 ParNew 收集器是激活 CMS 后(使用 -XX : +UseConcMarkSweepGC 选项)的默认新生代收集器,也可以使用-XX : +/-UseParNewGC 选项来强制指定或者禁用它。可以说直到CMS 的出现才巩固了 ParNew 的地位,但成也萧何败也萧何,随着垃圾收集器技术的不 断改进,更先进的G1 收集器带着 CMS 继承者和替代者的光环登场。 G1 是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作。所以自JDK 9开始, ParNew 加 CMS 收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1 所取代,甚至还取消了 ParNew加Serial Old以及Serial加CMS这两组收集器组合的支持 (其实原本也很少人这样使用),并直接取消了 -XX : +UseParNewGC 参数,这意味着 ParNew和CMS 从此只能互相搭配使用,再也没有其他收集器能 够和它们配合了。读者也可以理解为从此以后,ParNew 合并入 CMS ,成为它专门处理新生代的组成部分。ParNew可以说是 HotSpot 虚拟机中第一款退出历史舞台的垃圾收集器。
4.Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是 能够并行收集的多线程收集器……Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,那它有什么特别之处呢?定义:
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能 地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,
即:如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100 分钟,其中垃圾收集花掉 1 分 钟,那吞吐量就是99% 。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良 好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算 任务,主要适合在后台运算而不需要太多交互的分析任务。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量, 分别是控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数 。 -XX: MaxGCPauseMillis 参数允许的值是一个大于 0 的毫秒数,收集器将尽力保证内存回收花费的 时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得 系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集 300MB 新生代肯定比收集 500MB 快,但这也直接导致垃圾收集发生得 更频繁,原来10 秒收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、每次停顿 70 毫秒。停顿时间 的确在下降,但吞吐量也降下来了。
5. Serial Old收集器
工作图:
定义:
Serial Old是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记 - 整理算法。这个收 集器的主要意义也是供客户端模式下的HotSpot 虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与 Parallel Scavenge 收集器搭配使用 ,另外一种就是作为 CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。需要说明一下, Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非直接调用Serial Old收集器 ,但是这个 PS MarkSweep 收集器与 Serial Old 的实现几乎是一样的,所以在官方的许多资料中都是直接以Serial Old 代替 PS MarkSweep 进行讲解,这里笔者也采用这种方式。
6. Parallel Old收集器
工作图:定义:Parallel Old是 Parallel Scavenge收集器的老年代版本 ,支持多线程并发收集,基于标记 - 整理算法实现。这个收集器是直到JDK 6 时才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge 收集器,老年代除了 Serial Old ( PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如 CMS 无法与它配合工作。由于老年代Serial Old收集器在服务端应用性能上的 “ 拖累 ” ,使用 Parallel Scavenge 收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处 理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一 定比ParNew 加 CMS 的组合来得优秀。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
7.CMS收集器(重点)
工作图:定义:CMS( Concurrent Mark Sweep )收集器是一种以 获取最短回收停顿时间为目标 的收集器。目前很 大一部分的Java 应用集中在互联网网站或者基于浏览器的 B/S 系统的服务端上,这类应用通常都会较为 关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS 收集器就非 常符合这类应用的需求。从名字(包含“Mark Sweep” )上就可以看出 CMS 收集器是基于 标记-清除算法 实现的,它的运作 过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:1)初始标记(CMS initial mark )2)并发标记( CMS concurrent mark )3)重新标记( CMS remark )4)并发清除( CMS concurrent sweep )其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下 GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重 新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 ,这个阶段的停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的 对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一 起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。优点:CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来: 并发收集(重点)、低停顿,一些官方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度缺点:1.CMS收集器对处理器资源非常敏感。
事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量。
2.CMS收集器无法处理“浮动垃圾”(Floating Garbage)
在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分 垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集 时再清理掉。这一部分垃圾就称为“浮动垃圾”。
同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
3.会有大量空间碎片产生
CMS是一款基于“标记-清除”算法实现的收集器,如果对我的上一篇垃圾收集算法还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
8.Garbage First收集器(G1)(重点)
工作图:定义:Garbage First(简称 G1 )收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器 面向局部收集的设计思路和基于Region的内存布局形式 。早在 JDK 7 刚刚确立项目目标、 Oracle 公司 制定的JDK 7 RoadMap 里面, G1 收集器就被视作 JDK 7 中 HotSpot 虚拟机的一项重要进化特征。从 JDK 6 Update 14开始就有 Early Access 版本的 G1 收集器供开发人员实验和试用,但由此开始 G1 收集器的 “ 实 验状态” ( Experimental )持续了数年时间,直至 JDK 7 Update 4 , Oracle 才认为它达到足够成熟的商用 程度,移除了“Experimental” 的标识;到了 JDK 8 Update 40 的时候, G1 提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。这个版本以后的G1 收集器才被 Oracle 官方称为 “ 全功能的垃圾收集器 ” ( Fully-Featured Garbage Collector )。与其他垃圾收集器的不同点 :作为CMS 收集器的替代者和继承人,设计者们希望做出一款能够建立起 “ 停顿时间模型 ” ( Pause Prediction Model)的收集器,停顿时间模型的意思是 能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标 ,这几乎已经是实时 Java ( RTSJ )的中 软实时垃圾收集器特征了。那具体要怎么做才能实现这个目标呢?首先要有一个思想上的改变,在G1收集器出现之前的所有其他收集器,包括CMS 在内,垃圾收集的目标范围要么是整个新生代( Minor GC ),要么就是整个老 年代(Major GC ),再要么就是整个 Java 堆( Full GC )。而G1跳出了这个樊笼,它可以面向堆内存任 何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而 是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。G1开创的基于 Region的堆内存 布局是它能够实现这个目标的关键。虽然 G1 也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1 不再坚持固定大小以及固定数量的分代区域划分,而是把 连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。(重点) 收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。优点:相比CMS,G1的优点有很多,暂且不论可以指定最大停顿时间、分Region的内存布局、按收益动 态确定回收集这些创新性设计带来的红利,单从最传统的算法理论上看,G1也更有发展潜力。与CMS 的“标记-清除”算法不同, G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存 空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大 对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。缺点:G1无论是为了垃圾收集产生的内存占用( Footprint )还是程序运行时的额外执行负载( Overload )都要比 CMS 要高。