详解JVM的垃圾回收算法和垃圾回收器

51CTO
关注

下面就来介绍这几个垃圾回收器:

Serial回收器

Serial(串行)回收器采用复制算法的新生代收集器,它是一个单线程回收器,针对一个CPU或一条收集线程去完成垃圾收集工作,它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止,这个做法也称为 “Stop The World”。

如图8 所示,左边多个应用程序线程在执行, 当Serial 回收器的GC线程(虚线部分)执行的时候,应用程序线程(左边多个实线)都会暂停,只有在回收器线程执行完毕以后,应用程序线程(右边多个实线)才会继续执行。

图 8 串行垃圾回收器

该回收器的问题就是在进行垃圾回收时其他工作线程必须停顿,不过它在HotSpot虚拟机运行的Client模式下可以为新生代回收器服务。它的简单而高效对于限定单个CPU的环境来说,Serial回收器没有线程交互的开销。在用户的桌面应用场景中,分配给虚拟机管理的内存不大,停顿时间可以控制在几十毫秒以内,还是可以接收。它对于运行在Client模式下的虚拟机来说是一个很好的选择。

ParNew 回收器

ParNew回收器是Serial回收器的多线程版本,它也是一个新生代回收器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等。

如图9 所示,与Serial 不同的是ParNew 使用多个线程(中间多条虚线)的方式进行垃圾回收。

图9 ParNew 并行回收器

ParNew 回收器在多CPU环境下垃圾回收的效率会有明显提高。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置。反过来,如果针对单个CPU的环境 ParNew 和Serial 回收器的效果就难分伯仲了。

Serial Old回收器

Serial Old 是 Serial回收器的老年代版本,是单线程收集器,使用标记整理(Mark-Compact)算法。它可以给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old回收器

Parallel Old回收器是Parallel Scavenge的老年代版本,使用多线程的标记整理算法。在JDK 1.6中才开始提供,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择。Parallel Old回收器的工作流程与Parallel Scavenge相同。

Parallel Scavenge 回收器

Parallel Scavenge回收器是并行的多线程新生代回收器,它使用复制算法。Parallel Scavenge回收器的目标是达到一个可控制的吞吐量(Throughput)。

这里稍微说明一下, 吞吐量就是CPU运行用户代码时间与CPU总消耗时间的比值,表现成工时是:吞吐量 = 用户代码运行时间 /(用户代码运行时间 + 垃圾回收时间)。用户代码运行时间95 分钟,垃圾回收时间为5分钟,那吞吐量就是95/(95+5)=95%。

高吞吐量说明垃圾回收器高效率地利用CPU时间,尽快完成程序的运算任务。从而让垃圾回收造成的停顿时间变短,适合与用户交互的程序提升用户体验。

Parallel Scavenge会提供精确控制吞吐量的参数,此外还通过对参数-XX:+UseAdaptiveSizePolicy 设置来自动调节新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等信息。

此外Parallel Scavenge 回收器还有一个特点就是,会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,我们称之为GC自适应的调节策略(GC Ergonomics)。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是以获取最短回收停顿时间为目标的回收器,它适用于重视响应速度的应用场景,它是基于标记清除算法而实现的。

如图10 所示,从左往右CMS工作流程分为以下4个步骤:

· 初始标记(CMS initial mark):标记GC Roots直接关联到的对象,需要执行“Stop The World”,也就是让工作线程暂停。

· 并发标记(CMS concurrent mark):从GC Roots 查找所有可达的对象,这个过程耗时比较长,此时用户线程依旧在运行。

· 重新标记(CMS remark):修正并发标记期间,用户程序继续运作而导致标记的对象,并且调整标记记录,此阶段也需要“Stop The World”,因为不暂停工作线程的话还可能有标记不准确的情况发生。

· 并发清除(CMS concurrent sweep):对于标记不可用的对象进行并发清除操作,这个过程耗时会比较长,此时工作线程依旧可以运行。

所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间:

图10 CMS 垃圾回收器

CMS的优点明显,并发收集、低停顿。不过他对CPU资源非常敏感,在并发阶段虽然不会导致用户线程暂停,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。

CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%。

无法处理浮动垃圾(Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在垃圾回收阶段,用户线程还在运行,还需要预留有足够的内存空间给用户线程使用,因此CMS需要预留一部分空间提供并发收集时的程序运作使用。标记清除算法本身也会导致产生大量的空间碎片。

G1回收器

G1(Garbage-First)回收器是面向服务端应用的垃圾回收器,它具备如下特点:

· 充分利用多CPU缩短“Stop The World”停顿时间,可以通过并发的方式让Java程序继续执行。

· 不需要其他回收器配合就能独立管理整个GC堆,采用不同方式去处理新创建的对象和已存活一段时间、对于经历过多次GC的旧对象来说会有更好的回收效果。

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

· 可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒。

与其他垃圾回收器不同的是,G1回收的范围横跨整个堆内存。

如图11所示,G1将堆划分为多个大小相等的区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而是Region的集合。

图11 G1 将堆进行分Region

前面提到G1回收器可以预测的停顿时间,是因为它避免在整个Java堆中进行全区域的垃圾回收。G1会跟踪各个Region的垃圾堆积的价值大小(回收的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的回收时间,优先回收价值最大的Region。

虽然G1把Java堆分为多个Region,在某个Region中的对象可以与位于其他Region中的任意对象发生引用关系。在做可达性分析时仍然需要扫描整个堆,显然这样做效率是不高的。为了避免全堆扫描, G1为每个Region维护了一个记忆集(Remembered Set)。当发现程序在对引用(Reference)类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作。然后检查引用(Reference)对象是否处于不同的Region之中,如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的记忆集(Remembered Set)之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。说白了就是通过Remembered Set 记录跨Region引用的对象,其目的是避免全堆扫描。

如图12所示,Region2 中的两个对象分配被Region1 和Region3 中的对象引用,因此在Region2中的记忆集(Remembered set)就会记录这两个引用的信息,在垃圾回收的时候只需要收集记忆集的信息就知道对象在每个Region 的引用关系了,并不需要扫描所有堆的Region。

图12 跨Region的对象引用

说了G1 的特点和机制,下面通过图13 来看看它的执行过程:

· 初始标记(Initial Marking):标记GC Roots 能直接引用的对象,让下一阶段用户程序并发运行时,能在正确的Region中创建对象,此阶段需要停顿线程,但耗时很短。

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

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

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

总结

今天给大家介绍了垃圾回收的算法和JVM的垃圾回收器,算法作为思路和方法论的指导,而垃圾回收器是方法论的最佳实践,这里通过一张表格将两者进行一个总结:

      作者介绍

崔皓,51CTO社区编辑,资深架构师,拥有18年的软件开发和架构经验,10年分布式架构经验。曾任惠普技术专家。乐于分享,撰写了很多热门技术文章,阅读量超过60万。《分布式架构原理与实践》作者。

       原文标题 : 详解JVM的垃圾回收算法和垃圾回收器

声明: 本文系OFweek根据授权转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们。
侵权投诉

下载OFweek,一手掌握高科技全行业资讯

还不是OFweek会员,马上注册
打开app,查看更多精彩资讯 >
  • 长按识别二维码
  • 进入OFweek阅读全文
长按图片进行保存