深入理解java虚拟机-垃圾回收算法和垃圾回收器 一、垃圾回收算法 二、垃圾收集器 三、垃圾收集器参数总结 四、内存分配和回收策略 五、虚拟机性能监控、故障处理工具

1.1、分代收集

新生代,标记复制

老年代,标记整理(ParallelScavenge)或标记清除(CMS)

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意Major GC这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

1.2、标记清除

先标记,后清除,缺点是会产生内存碎片,用于老年代多一些。

1.3、标记复制

新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将EdenSurvivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认EdenSurvivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%Eden80%加上一个Survivor10%),只有一个Survivor空间,即10%的新生代是会被“费”的。

1.4、标记整理

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的标记-整理Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,“标记-整理”算法的示意图如图3-4所示。标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。

二、垃圾收集器

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具

图3-6展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用[3],图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。

HotSpot虚拟机里面关注吞吐量ParallelScavenge收集器是基于标记-整理算法的,而关注延迟CMS收集器则是基于标记-清除算法的。

2.1、Serial收集器

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

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具

2.2、ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。ParNew收集器的工作过程如图3-8所示。

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具

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

JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。遗憾的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器ParallelScavenge配合工作[1],所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。

可以说直到CMS的出现才巩固了ParNew的地位,但成也萧何败也萧何,随着垃圾收集器技术的不断改进,更先进的G1收集器带着CMS继承者和替代者的光环登场。G1是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作。

所以自JDK 9开始,ParNewCMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了ParNewSerial Old以及SerialCMS这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了-XX:+UseParNewGC参数,这意味着ParNewCMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。读者也可以理解为从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部分。ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。

2.3、Parallel Scavenge收集器

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

那它有什么特别之处呢?

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

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

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具

由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。

2.4、Serial Old收集器

Serial OldSerial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:

一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中继续讲解。

Serial Old收集器的工作过程如图3-9所示

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具

需要说明一下,Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非直接调用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解,这里笔者也采用这种方式。

2.5、Parallel Old收集器

Parallel OldParallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PSMarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比ParNewCMS的组合来得优秀。

直到Parallel Old收集器出现后,吞吐量优先收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel ScavengeParallel Old收集器这个组合。Parallel Old收集器的工作过程如图3-10所示。

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具

2.6、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”。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的记记录(详见3.4.6节中关于增量更新的讲解),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过图3-11可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具

2.7、Garbage First收集器

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。早在JDK 7刚刚确立项目目标、Oracle公司制定的JDK 7 RoadMap里面,G1收集器就被视作JDK 7HotSpot虚拟机的一项重要进化特征。从JDK6 Update 14开始就有Early Access版本的G1收集器供开发人员实验和试用,但由此开始G1收集器的“实验状态”(Experimental)持续了数年时间,直至JDK 7 Update 4Oracle才认为它达到足够成熟的商用程度,移除了Experimental的标识;到了JDK 8 Update 40的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。这个版本以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。

G1是一款主要面向服务端应用的垃圾收集器。HotSpot开发团队最初赋予它的期望是(在比较长期的)未来可以替换掉JDK 5中发布的CMS收集器。现在这个期望目标已经实现过半了,JDK 9发布之日,G1宣告取代Parallel ScavengeParallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器[1]。

如果对JDK 9及以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。

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采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角[1]”。三者总体的表现会随技术进步而越来越好,但是要在这三个方面同时具有卓越表现的“完美”收集器是极其困难甚至是不可能的,一款优秀的收集器通常最多可以同时达成其中的两项。

在内存占用、吞吐量和延迟这三项指标里,延迟的重要性日益凸显,越发备受关注。其原因是随着计算机硬件的发展、性能的提升,我们越来越能容忍收集器多占用一点点内存;硬件性能增长,对软件系统的处理能力是有直接助益的,硬件的规格和性能越高,也有助于降低收集器运行时对应用程序的影响,换句话说,吞吐量会更高。但对延迟则不是这样,硬件规格提升,准确地说是内存的扩大,对延迟反而会带来负面的效果,这点也是很符合直观思维的:虚拟机要回收完整的1TB的堆内存,毫无疑问要比回收1GB的堆内存耗费更多时间。由此,我们就不难理解为何延迟会成为垃圾收集器最被重视的性能指标了。

三、垃圾收集器参数总结

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具

四、内存分配和回收策略

HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中(详见第2章)。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

五、虚拟机性能监控、故障处理工具

5.1、可视化工具

主要是JMC(Java Mission Control)

jconsole、jvisualvm

5.2、命令行工具

深入理解java虚拟机-垃圾回收算法和垃圾回收器
一、垃圾回收算法
二、垃圾收集器
三、垃圾收集器参数总结
四、内存分配和回收策略
五、虚拟机性能监控、故障处理工具