Java垃圾收集器详解

1 概述

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

JVM入门

Java垃圾回收算法详解

如果说垃圾回收算法是虚拟机中垃圾回收的理论,那么垃圾收集器就是针对于这些理论的具体实现,并且不同厂商和不同版本的虚拟机所提供的垃圾收集器可能会有区别,所以本文主要基于HotSpot虚拟机进行讲解。

1.1 前置知识

1.1.1 Stop The World

进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束为止,这个过程称为 Stop The world。

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

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

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

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

2 垃圾收集器

在这里插入图片描述

上图所示主要展示了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

2.1 Serial

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

在这里插入图片描述

上图所示为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垃圾收集器。

2.2 ParNew收集器

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

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

在这里插入图片描述

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

2.3 Parallel Scavenge收集器

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

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

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

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

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

2.4 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收集器的后备预案。

在这里插入图片描述

2.5 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资源敏感并且与用户交互少的场景下,可优先使用该组合。

在这里插入图片描述

2.6 CMS收集器

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

在这里插入图片描述

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收集器的内存回收过程是与用户线程一起并发执行的。

2.6.1 存在问题

  1. 对CPU资源非常敏感

在并发标记和并发清除阶段,虽然CMS收集器不会造成用户线程停顿,但是由于垃圾回收占用了一部分线程(CPU资源),会导致程序变慢。

CMS默认启动的垃圾回收线程是(CPU数量 + 3) / 4,当CPU数量在4个以上时,回收线程占不少于25%的CPU资源,并且随着CPU数量的增加而下降,但是当CPU数量不足4个时,回收线程就会占到大于50%的CPU资源。

示例:

CPU数量=9,回收线程 = (9 + 3) / 4 = 3,回收线程占CPU资源:3 / 9 = 0.3

CPU数量=5,回收线程 = (5 + 3) / 4 = 2,回收线程占CPU资源:2 / 5 = 0.4

CPU数量=3,回收线程 = (3 + 3) / 4 = 1,回收线程占CPU资源:1 / 3 = 0.33

CPU数量=2,回收线程 = (2 + 3) / 4 = 1,回收线程占CPU资源:1 / 2 = 0.5

CPU数量=1,回收线程 = (1 + 3) / 4 = 1,回收线程占CPU资源:1 / 1 = 1

由上可见,当CPU数量在大于4时,CPU数量越多,回收线程占用的CPU资源越少,而当CPU数量小于4时,回收线程占用的CPU资源越多。

  1. 无法处理浮动垃圾

由于在并发清理阶段用户线程同样在执行,而用户线程的执行就肯定会产生垃圾,而产生的这些垃圾在标记阶段之后,并没有进行标记,所以清理阶段不会对这些垃圾进行清理,只能留到下一次的垃圾回收中再进行清理,这部分的垃圾就是浮动垃圾(Floating Garbage)。

由于CMS在垃圾清理阶段用户线程还在运行,所以就需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会进行垃圾回收,也就是预留了32%的内存空间供用户线程使用,在JDK1.6中,CMS收集器的启动阈值提升至92%。如果CMS垃圾回收阶段预留的内存无法满足用户线程的需要,就会发生“Concurrent Mode Failure”失败,这是就会启动CMS的后备预案Serial Old来进行垃圾回收,但是这样垃圾回收的停顿时间就会变长。

  1. 内存碎片

由于CMS采用的是标记清除算法,在前面文章中有过介绍,标记清除算法的缺点就是会产生内存碎片。

为了解决内存碎皮的问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。

虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着执行一次带压缩的Full GC(默认值为0,表示每次进入Full GC时都进行碎片整理)。

2.7 G1收集器

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

在这里插入图片描述

上图所示的就是通过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,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

在这里插入图片描述

参考资料:

《深入理解Java虚拟机》 周志明著

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodeJR

如果觉得有用请赏一个呗!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值