垃圾收集器详解(7种主流垃圾收集器)

之前的文章介绍了JVM的基础入门以及Java垃圾回收算法的详解,本章的内容主要介绍在Java中常用到的垃圾收集器。

串行、并行和并发垃圾收集

串行:用户线程和垃圾回收线程交替执行,当执行垃圾回收线程时暂停用户线程,因此会出现Stop The World,并且垃圾回收线程是单线程的,并不是CPU是单核,在多核CPU下串行垃圾回收也是单线程。

并行:和串行基本一致,唯一不同的就是垃圾回收是多线程并行进行,不再是单线程,需要在多核CPU环境下,用户线程会被暂停,同样出现Stop The World。

并发:在多核CPU环境下,用户线程和垃圾回收线程同时执行,也就是在同一时刻,CPU0执行用户线程,CPU1执行垃圾回收线程。

垃圾收集器介绍

垃圾收集器详解(7种主流垃圾收集器)-mikechen

上图所示主要展示了7种垃圾收集器,按照它们所工作的区域划分为新生代和老年代。

两个收集器之间的连线表示这两个收集器之间可以配合使用,一个用于新生代一个用于老年代,CMS和Serial Old的连线表示Serial Old作为CMS出现“Concurrent Mode Failure”失败的后备预案。

新生代和老年代:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge
  • 老年代收集器:Serial Old、Parallel Old、CMS
  • 整堆收集器:G1

串行、并行和并发

  • 串行:Serial、Serial Old
  • 并行:ParNew、Parallel Scavenge、Parallel Old
  • 并发:CMS、G1

垃圾回收算法

  • 标记复制算法:Serial、ParNew、Parallel Scavenge、G1
  • 标记清除算法:CMS
  • 标记整理算法:Serial Old、Parallel Old、G1

Serial垃圾收集器

Serial是一个新生代收集器,使用的是标记复制算法,Serial收集器是最基础也是历史最悠久的收集器,Serial是属于单线程垃圾回收器,在进行垃圾回收时会暂停所有的用户线程,直到垃圾回收结束(Stop The World)。

垃圾收集器详解(7种主流垃圾收集器)-mikechen

上图所示为Serial收集器工作示意图,Serial在JDK1.3之前是新生代垃圾回收的唯一选择,虽然Serial出现时间很久并且简单,但是一直都是HotSpot虚拟机运行在Client模式下的默认新生代收集器。

TIPS:JVM有两种运行模式Server与Client。两种模式的区别在于,Client模式启动速度较快,Server模式启动较慢;但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。这是因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。

32位默认为Client模式,64位只支持Server模式。

优势:相比于其他垃圾收集器的单线程,Serial更加简单高效,在单个CPU环境下,Serial收集器不需要进行额外的线程切换,有更高的效率。

设置参数: -XX:+UseSerialGC:指定使用Serial垃圾收集器。

ParNew垃圾收集器

ParNew是新生代收集器,使用的算法和Serial一样都是标记复制算法,ParNew收集器就是Serial收集器的多线程版本, 两者的区别就是Serial使用单线程进行垃圾回收,ParNew使用多条线程进行垃圾回收,两者使用的垃圾回收算法、Stop The World和回收策略等其他方面都完全一样。

ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。

垃圾收集器详解(7种主流垃圾收集器)-mikechen

ParNew收集器在单CPU环境下的垃圾收集效率不会比Serial更好,因为ParNew存在线程交互的开销,然而,随着可以使用的CPU的数量的增加,ParNew收集器对资源的有效利用越来越好。

Parallel Scavenge垃圾收集器

Parallel Scavenge是一个新生代收集器,使用的算法是标记复制算法,和ParNew一样属于并行垃圾收集器,其运行示意图可看ParNew收集器示意图。

Parallel Scavenge的特点在于它的关注点和其他收集器不同,在之前介绍的收集器以及后面要介绍的CMS收集器中,更加关注的是如何减少用户线程的停顿时间(Stop The World),而Parallel Scavenge关注的是吞吐量(Throughput)。

吞吐量:运行用户代码时间和CPU总消耗时间比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。

例如一个CPU运行用户代码时间是90分钟,垃圾收集时间是10分钟,那么吞吐量 = 90 / (90 + 10) = 90%

停顿时间短的垃圾收集器适合与用户交互频繁的程序,较少的停顿能够提高用户体验,而吞吐量大的垃圾收集器可以高效率利用CPU,主要适用于后台运算而不需要太多交互的内容。

 Serial Old垃圾收集器

Serial Old收集器就是Serial收集器的老年代版本,同样是一个单线程的收集器,使用的是标记整理算法,这个收集器是虚拟机在Client模式下的老年代默认收集器,如果在JDK1.5之前版本的Server模式下,Serial Old收集器和Parallel Scavenge收集器搭配使用,Parallel Scavenge收集器架构中本身有PS MarkSweep收集器作为老年代收集器,但是PS MarkSweep收集器与Serial Old实现非常接近,所以直接以Serial Old代替PS MarkSweep进行讲解。在JDK1.5版本之后Serial Old收集器则作为CMS收集器的后备预案。

垃圾收集器详解(7种主流垃圾收集器)-mikechen

Parallel Old垃圾收集器

Parallel Old收集器收集器是Parallel Scavenge收集器的老年代版本,使用的是标记整理算法,在JDK1.6中开始提供,在JDK1.6之前,如果新生代收集器选择了Parallel Scavenge,老年代就只能选择Serial Old(PS MarkSweep)收集器,由于老年代Serial Old收集器使用单线程在服务端应用性能上的拖累,即便使用了Parallel Scavenge收集器,它的吞吐量效果也无法展现出来。

直到Parallel Old收集器出现,它的关注点同样在吞吐量,因此Parallel Scavenge和Parallel Old搭配使用,能最大程度的提升程序的吞吐量,在注重吞吐量和CPU资源敏感并且与用户交互少的场景下,可优先使用该组合。

垃圾收集器详解(7种主流垃圾收集器)-mikechen

CMS垃圾收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,采用的是标记清除算法。

垃圾收集器详解(7种主流垃圾收集器)-mikechen

CMS收集器在进行垃圾收集时,一共有四个步骤:

  1. 初始标记(CMS initial mark):单线程执行,标记出GC Roots能直接关联到的对象,速度较快,会发生Stop The World。
  2. 并发标记(CMS concurrent mark):并发执行,用户线程正常运行,垃圾收集线程进行并发标记,也就是GC Roots Tracing,对初始标记的对象进行追踪标记,该阶段时间较长,但是不会发生Stop The World。
  3. 重新标记(CMS remark):由于并发标记阶段用户线程仍在执行,所以可能会产生垃圾,重新标记阶段就是将并发标记阶段发生变动了的对象标记进行修正,该阶段会发生Stop The World,停顿时间比初始标记阶段稍长,比并发标记阶段短。
  4. 并发清除(CMS concurrent sweep):并发执行,该阶段直接清楚之前标记的垃圾,用户线程可继续执行,不会产生Stop The World。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起 工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

G1垃圾收集器

G1(Garbage – First)垃圾收集器是面向服务端的垃圾收集器,使用G1垃圾收集器时,Java堆空间中不再和之前一样划分为Eden、Survivor和Old区,而是将Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以是Eden、Survivor或者Old,还有一种特殊的区域,叫Humongous区域,如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个大对象,就会分配到Humongous区域。

垃圾收集器详解(7种主流垃圾收集器)-mikechen

上图所示的就是通过Region划分的Java堆区,在该划分方法中,Eden、Survivor和Old不再是区分开的,虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(或CPU核心)来缩短Stop-The-World停顿的时间,其他收集器原本需要停顿用户线程执行的GC动作,G1收集器可以通过并发的方式让用户线程和GC线程同时执行。

空间整合:从整体来看,G1采用的是标记整理算法,从局部(两个Region间)看,是基于复制算法,所以G1收集器不会产生内存碎片。

可预测的停顿:低停顿的同时实现高吞吐量,建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这样就保证了在有限的时间内尽可能提高效率。

G1垃圾收集器工作时可以分为4个步骤,其中初始标记、并发标记以及最终标记和CMS收集器的前三个步骤有很多相似之处。

  1. 初始标记(Initial Marking)

标记出GC Roots能直接关联到的对象,修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象速度较快,会发生Stop The World。

  1. 并发标记(Concurrent Marking)

从GC Root开始对堆中对象进行可达性分析,找出存活的对象,与用户线程并发执行,耗时较长。

  1. 最终标记(Final Marking)

将并发标记阶段发生变动了的对象标记进行修正,将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,并行执行,会发生Stop The World。

  1. 筛选回收(Live Data Counting and Evacuation)
  • 首先对各个Region的回收价值和成本进行排序。
  • 然后根据用户所期望的GC停顿时间来制定回收计划。
  • 最后按计划回收一些价值高的Region中垃圾对象。
  • 回收时采用”复制”算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存。
  • 该阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
垃圾收集器详解(7种主流垃圾收集器)-mikechen

mikechen睿哥

mikechen睿哥,十余年BAT架构经验,资深技术专家,就职于阿里、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复面试即可获取《史上最全阿里Java面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法