Java虚拟机探究(一)—七大垃圾收集器


本文由作者通过《深入理解Java虚拟机》总结而来
转载注明出自bestsort.cn,谢谢合作


JVM 垃圾收集器

文中部分概念

  • 并行

并行计算(英语:parallel computing)一般是指许多指令得以同时进行的计算模式。在同时进行的前提下,可以将计算的过程分解成小部分,之后以并发方式来加以解决。

  • 并发

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任意时刻点上只有一个程序在处理机上运行

  • Minor GC: 新生代的垃圾回收

  • Major GC: 老年代的垃圾回收

  • Full GC:整堆包括新生代和老年代的垃圾回收

JVM常用的垃圾收集器有7种, 除了G1以外每种收集器只能作用于不同分代.

Serial

Serial 是最老的收集器, 在JDK1.3.1之前曾是新生代收集的唯一选择. 老就意味着历史遗留问题难以处理,这一点在Serial身上也有体现: Serial是一个单线程的收集器,也就是说它只能用一个CPU或一条线程去完成收集工作.在收集时,会暂停其他所有的工作进程. 这其实是很不可思议的. 试想, 在看电影看到酣畅淋漓的时候 Serial 告诉你: 停下来,我要开始GC了!尽管只有短短的几分钟,但是一场电影的观感体验肯定是大不如之前了。

虽然看起来Serial 已经没什么存在的必要了,但是实际上,它仍然是虚拟机运行在Client模式下的默认新生代收集器.相对于其他收集器而言,它足够简单,而且对于单线程环境来讲,由于没有线程交互的开销,Serial反而能获取最高的资源收集效率. 所以,Serial对于运行在Client模式下的虚拟机是一个比较好的选择

ParNew

ParNew 虽然有个新名字,但是它和Serial基本一致.包括Serial可用的控制参数,收集算法,对象分配规则,回收策略等都和Serial一致. 可以说两者是同一个模子里刻出来的.

不过,ParNew 是多线程的. 它的出现解决了Serial不能多线程作业的这个痛点.

还有值得一提的就是: 只有它和Serial能够和CMS(下文中会讲到,一款真正意义上支持并发的收集器) 配合工作.

同其他多线程收集器一样, ParNew在限制单线程环境下依旧打不过天生只支持单线程的Serial收集器(

甚至连超线程的Serial都打不过).

Parallel Scavenge

Parallel Scavenge 是用于新生代的收集器. 和ParNew一样:也是使用复制算法并且是多线程收集器. 不过相较于其他收集器而言,Parallel Scavenge 的关注点并不在于怎样缩短GC时用户线程的停顿时间,而是达到一个可控制的吞吐量($吞吐量 = 运行用户代码时间/(运行用户代码时间 + GC时间)*100%$).

Parallel Scavenge 提供了以下两个参数以用于控制吞吐量

  • -XX:MaxGCPauseMillis : 控制最大GC停顿时间

  • -XX:GCTimeRatio: 直接设置吞吐量大小($0< n <100$ 为GC时间占总时间的比率)

此外, Parallel Scavenge 还提供了-XX:+UseAdaptiveSizePolicy以动态选择最大的吞吐量/最合适的停顿时间,自适应策略也是Parallel Scavenge和ParNew收集器的一个重要区别

Serial Old

顾名思义, Serial Old是Serial的老年代版本。同Serial一样, 它也是单线程收集器。用的是“标记——整理”算法。主要用于Client模式下的虚拟机。如果是Server模式的话,他也可以用于以下用途

  • JDK1.5之前和Parallel Scavenge搭配使用(虽然Parallel Scavenge自身架构有用于老年代GC的PS MarkSweep,但是这两个收集器的实现方式非常接近)

  • 作为CMS的备用方案,在并行收集发生Concurrent Mode Failure 的时候使用

Parallel Old

Parallel Old 是 Parallel Scavenge 的老年代版本,使用多线程和“标记——整理算法”。这个收集器是JDK1.6中才开始提供的,在此之前Parallel Scavenge可谓是处于一个非常尴尬的地位:在此之前要使用Parallel Scavenge的话只能搭配Serial Old(PS MarkSweep) 收集器。但是Serial Old在服务端应用性能上的低效(我们前面指出过Serial Old只能用于单线程),使得即使使用Parallel Scavenge也不能在整体应用上获得吞吐量最大化的效果。不过在Parallel Old出现过后,Parallel Scavenge + Parallel Old的搭配组合有了比较明确的应用场景。在注重吞吐量和CPU资源敏感的场合,都可优先考虑这个组合

CMS

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,对于采用B/S架构的应用,CMS能很好的满足“流畅”这一需求,给用户带来较好的体验。CMS采用“标记——清除”算法,不过相较于前面几种收集器,CMS更复杂一些,整个GC过程分为以下几个步骤

  • 初始标记(CMS inital mark) 标记GC Roots 能直接关联到的对象

  • 并发标记(CMS concurrent mark)进行GC Roots Tracing的过程,在整个过程中耗时最长

  • 重新标记(CMS remark)为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短

  • 并发清除(CMS concurrent sweep)

其中,初始标记重新标记**仍然需要Stop The World

优点

  • 并发收集

  • 低停顿

缺点

对CPU资源非常敏感.

在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。

为了解决这种问题,虚拟机还提供了一种“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS).但是其效果很一般,目前,i-CMS已经不再提倡用户使用

无法处理浮动垃圾(Floating Garbage)

可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”

也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。当CMS运行期间预留的内存无法满足程序需要时,就会出现一次“ConCurrent Model Failure失败,这时候虚拟机将会临时启用Serial Old 来进行老年代的垃圾收集,这时候停顿时间就很长了

标记-清除算法导致的空间碎片

CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。

G1

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一(14年开始商用),它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  • 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

  • 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。

  • 空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

横跨整个堆内存

在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合

建立可预测的时间模型

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

避免全堆扫描——Remembered Set

G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。

为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。


如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行

  • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行

  • 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

总结

收集器运行方式应用的年代算法目标适用场景
Serial单线程新生代复制响应速度优先单CPU下的Client模式
Serial Old单线程老年代标记-整理响应速度优先单CPU下的Client模式,CMS的后背预案
ParNew多线程,并行新生代复制响应速度优先多CPU环境在Server模式下与CMS配合
Parallel Scavenge多线程,并行新生代复制吞吐量优先在后台运算且不需要太多交互的任务
Parallel Old多线程,并行老年代标记-整理吞吐量优先在后台运算且不需要太多交互的任务
CMS多线程,并发老年代标记-清除响应速度优先B/S架构
G1多线程,并发新生代&&老年代标记-整理+复制相应速度优先面向服务端应用,以后会逐步替换CMS

碎碎念

虽然是国庆七天乐,但是我真的是写了七天BUG啊QAQ

最近因为写项目的关系也没时间更新博客了

也抽不出太多时间去看书

我太难了

觉得文章不错的话可以请我喝一杯茶哟~
  • 本文作者: bestsort
  • 本文链接: https://bestsort.cn/2019/10/06/106/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-SA 许可协议。转载请注明出处!并保留本声明。感谢您的阅读和支持!
-------------本文结束感谢您的阅读-------------