首页 > 代码库 > 【5】JVM-垃圾收集器

【5】JVM-垃圾收集器

通过学习了解到现在商用的JVM中的垃圾收集采用的是分代收集算法,即针对不同年代采用不同的收集算法。在JVM中,GC主要作用于堆内存中,堆内存又被划分为新生代和老年代,由于新生代对象绝大多数是朝生夕死,而老年代相对存活时间就很长,故而需要使用不同的垃圾收集机制,所以垃圾收集器也就分为新生代收集器和老年代收集器,两者相互组合进行JVM堆内存的空间回收(下图中相连的垃圾收集器表示可以相互组合,注意Serial Old和CMS也可以联合进行老年代的垃圾收集)。JDK6u14中开始测试的G1垃圾收集器,正式发布于JDK7u4中,是目前唯一不需要依赖其他垃圾收集器即可完成新生代和老年代内存收集。阅读之前先了解,GC的两个指标:暂停时间-应对与存在大量用户交互的场景;吞吐量-应对后台计算任务。

 

  • 新生代的垃圾收集器有:Serial收集器、ParNew收集器、Parallel Scavenge收集器
  • 老年代的垃圾收集器有:Serial Old收集器、Parallel Old收集器、CMS收集器
  • G1收集器。http://f.dataguru.cn/thread-514678-1-1.html

 

技术分享 

笔者使用的是JDK7u51,也就是JDK1.7.0_51

    下面我将试着通过自己的理解来分析各个垃圾收集器的特点,目前并没有一个适用于任何场景的垃圾收集器,所以选择何种垃圾收集器进行配合是根据具体应用来区别对待的,那么了解各种垃圾收集器的特点以及他们之间是否可以相互配合,就十分重要了。

    垃圾收集器运行过程中必然会发生“Stop the world”,只是时间长短和暂停时间可不可控的区别。

    在进行下面的阅读之前,首先明确在垃圾收集器中,“并发”和“并行”这两个概念的差别:

 

  1. 并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态
  2. 并发(Concurrent):垃圾收集线程和用户线程同时执行(不一定是并行,可能是交替执行),用户程序继续执行,而GC运行在另一个CPU上

 

 

 

  • Serial

 

       Serial垃圾收集器,通过这个单词的意思“连续”,我认为这个应该是指的GC之后内存空间不存在内存碎片的意思,那么必然不会采用“标记-清除算法”来实现,所以这个垃圾收集器在新生代使用的是“复制算法”,而Serial Old作为Serail收集器的老年代版本,使用的就是“标记-整理算法”。

    为什么先说Serial垃圾收集器,是因为这个收集器是最基本、历史最悠久的收集器,在JDK1.3.1之前,是JVM新生代收集的唯一选择。Serial收集器是一个单线程的收集器,这个“单线程”是指JVM在使用它进行GC的时候,必须暂停其他所有的工作线程(sun将这件事情称为“Stop the world”),直到GC完成,这是一件非常可怕的事情。看到这里,你可能想我一定要修改我的JVM的新生代收集器,不用Serial了,但是直至现在,Serial依然是JVM在运行Client模式下默认的新生代 收集器。与其他垃圾收集器的单线程相比,Serial简单而高效。对于用户桌面应用场景来说,分配给JVM的内存一般不会太大,收集十几甚至一两百兆的内存,停顿时间可以控制在几十毫秒,最多一百多毫秒以内,只要不是特别频繁,这些停顿还是可以接受的。所以,对于Client模式下的JVM来说,Serial是个很好的新生代收集器,简单高效。

技术分享

 

  • ParNew-Parallel New

       ParNew收集器也是一个新生代收集器,其实就是Serial收集器的多线程版本,是一个“并行”的垃圾收集器,除了多线程外,其他和Serial差不多。想想也就明白了,当JVM团队开发出来了Serial,可以满足Client模式下的JVM,但是对于Server模式下的JVM来说,运行很长时间,有很多的对象需要收集(可能几十个G),单线程导致的停顿时间太长了(比如每运行1小时需要停顿5分钟),用户无法接受业务线程停顿那么长的时间,我猜测这种情况下那些大牛能想到的最简单的办法就是让Serial变成多线程,这样开多个线程就可以有效的降低停顿时间,故而这个Serial的多线程版本也就诞生了。

    ParNew是许多运行在Server模式下的JVM中首选的垃圾收集器,其中一个重要原因就是除了Serial,它是唯一可以和CMS(Concurrent Mark Sweep)老年代垃圾收集器配合工作。

    ParNew在单CPU环境中收集效果不如Serial收集器,但是随着CPU的增加,它对于GC时系统资源的利用还是很有好处的,默认开启的线程数与CPU的数量一致 。

技术分享

 

 

 

  • Parallel Scavenge

 

    Parallel Scavenge收集器,简称PS收集器,它和ParNew收集器一样是一个多线程的并行新生代垃圾收集器,一样采用“复制算法”(始发于JDK1.4.0)。那为什么还要这个PS收集器呢?现在想一下,ParNew收集器为什么会产生,不就是闲Serial收集器导致的“Stop the world”的时间太长了嘛,搞个多线程,减少停顿时间。这种的垃圾收集器适合重视服务的响应速度的应用程序(比如购物网站,肯定希望停顿时间越短越好,这样用户体验才好),但是对于一个后台计算任务(比如MapReduce)来说,没有太多的交互任务,那么它所重视的就不是这种响应速度,而是CPU的有效时间利用率(这是我的理解),官方称之为“吞吐量(Throughtput)”。吞吐量就是指CPU用来运行用户代码的时间和CPU的总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+GC消耗的时间)。

    Parallel Scavenge收集器正是基于对“吞吐量”的追求而产生的,它的目标就是达到一个可控的吞吐量。由于与吞吐量关系密切,Parallel Scavenge收集器也被称为“吞吐量优先“收集器。Parallel Scavenge提供了两个参数用来精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数(单位:毫秒),以及直接设置吞吐量大小的-XX:GCTimeRatio参数(0-100之间,不包括首尾)。GCTimeRatio参数的计算规则是,比如设成19,那么允许最大时间就占总时间的5%,即1/(1+19),默认值是99,也就是默认允许最大GC时间占比是1%。

    Parallel Scavenge收集器还有一个参数来开启GC的自适应调节策略,只需要将JVM基本内存设置好,并且制定上述两个参数中的一个来作为JVM的优化目标,那么JVM就可以根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这个参数就是-XX:+UseAdaptiveSizePolicy。自适应调节策略也是PS收集器 相对于ParNew收集器的一个重要区别。ParNew收集器需要手工指定新生代大小(-Xmn)、Eden与Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。

技术分享

 

  • Serial Old
 
       Serial收集器的老年代版本,自然也是JVM中最早的老年代垃圾收集器,又称为PS MarkSweep。它与Serial收集器一样是一个单线程收集器,使用”标记-整理算法“。这个收集器的主要一样也是被Client模式下的JVM使用。如果在Server模式下,它主要有个两大用途:
  1. 在JDK1.5和之前的版本中与Parallel Scavenge收集器搭配使用,因为此时CMS还没有,CMS正式发布于JDK1.6中
  2. 在JDK1.6和以后的版本中,作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。
    现在JDK已经发展到了JDK8了,大量系统使用的稳定版本也是JDK1.6和JDK1.7,所以第一种用途几乎没什么意义了,第二种还在用。
 
PS:为何在JDK1.4的时候,新生代已经有了Serial、ParNew和Parallel Scavenge这三种收集器,而老年代此时还是只有Serial Old这一种最原始的单线程收集器,个人推测,是由于Young GC发生的频率远远高于Full GC,故而如何有效提高Young GC的收集效率减少停顿时间和增加吞吐量才是JDK1.4及之前的时间里JVM研发团队的首要任务,当对新生代的垃圾收集做到不错的程度的时候(有了并行收集器),工作重心才转移到老年代的垃圾收集上,这就是一个工作的优先级问题,毕竟每个人、每个团队、每个公司都避免不了的只有有限的精力和资源。在JDK1.5的时候推出了新的老年代收集器CMS,JDK1.6的时候推出了Parallel Old。
技术分享

 

  • CMS
        在面对新生代的时候,垃圾收集器有两种提升的方式,一种是减少用户线程暂停的时间,另一种是提高”吞吐量“。假设现在处于JDK1.4发布到1.5发布之间,你要设计一个老年代的垃圾收集器,当前只有Serial Old一种,那么如果让你来选,你优先提高GC的何种性能呢?前面也分析了”Stop the world“和”吞吐量“性能提升所应对的场景,那么再问你,你觉得优先提高JVM老年代垃圾收集来满足用户对响应速度的需求比较重要,还是优先提高JVM老年代垃圾收集来满足后台的计算任务呢?JDK1.4于2002年2月发布,那是JAVA已经比较火了,从96年开始JAVA就被用来制作网页了,那么显然用户对于响应速度的提升需求比较强烈,那么对于JVM项目组来说在已经有了3种新生代垃圾收集器的前提下,研发一款减少用户线程停顿时间的老年代垃圾收集器是必然的,所以在2004年9月,历时两年半,发布了JDK1.5,有了CMS垃圾收集器(Concurrent Mark Sweep)。(以上均为个人推测,与事实不符概不负责)
    CMS,Concurrent Mark Sweep,从名字就可以看出来这是一个基于”标记-清除“算法的并发垃圾收集器(注意并发和并行的区别),用于老年代的垃圾收集,它在Sun的一些文档中被称为并发低停顿收集器(Concurrent Low Pause Collector)。CMS收集器是一种以获取最短停顿时间为目标的收集器,优先满足重视服务响应速度的需求。CMS的运行过程相对前面几种收集器来说比较复杂,整个过程分为4步:
  1. 初始标记:仅仅标记一下GC Roots能直接关联到的对象,速度很快
  2. 并发标记:GC Roots Tracing,梳理引用链
  3. 重新标记:修正并发标记过程中,用户线程运行导致标记变动的那一部分对象的标记记录。
  4. 并发清除
 
    其中,初始标记和重新标记都需要”Stop the world“,但是整个过程中耗时最长的并发标记和并发清除阶段都是可以与用户线程一起工作的,所以总体来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。
    虽然CMS很优秀,是HotSpot虚拟机中有史以来的第一款真正意义上的并发收集器,但是CMS还远远达不到完美的程度,它有三个显著的缺点:
  1. 对CPU资源非常敏感
  2. 无法处理浮动垃圾,所谓的浮动垃圾就是CMS并发清除阶段用户线程运行产生的垃圾,这部分垃圾必须等待下一次的垃圾收集来清除。所以CMS执行GC的时候需要预留足够的内存空间(默认32%,可调节)给用户线程使用,如果预留空间无法满足用户线程的内存需求,那么就会发生“Concurrent Mode Failure”失败,然后虚拟机就会启动Serial Old来重新进行老年代的垃圾收集,这样就会导致停顿时间很长了。
  3. 会产生空间碎片(”标记清除“算法的特点),CMS在Full GC发生之后附带了一次碎片整理过程,而内存整理是无法并发的,导致停顿时间不得不变长。发生这个问题的时候,可能就会调用Serial Old来处理老年代的垃圾回收了。
 
技术分享
 

 

  • Parallel Old
 

    Parallel Old是Parallel Scavenge收集器的老年代版本,简称PS Old,使用了多线程和”标记-整理“算法,这个收集器是在JDK1.6中才提供的,在此之前PS new的地位比较尴尬,因为在此之前老年代的垃圾回收只有Serial Old这一种收集器,与Serial Old配合,Parallel Scavenge无法产生理想的回收效果,吞吐量在老年代很大且硬件比较高级的环境中可能还不如使用ParNew与CMS的组合”给力“,而PS Old产生之后,PS New才变得名副其实。
    也就是说,在JDK1.6及之后,在注重吞吐量和CPU资源敏感的场合,都可以优先考虑PS New和PS Old的组合。

 
技术分享
 
 

 

  • G1

       在JDK6u14中提供了Early Access版本的G1收集器以供试用,直到JDK7u4的时候才正式发布。G1是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是未来替换掉CMS收集器,从这点上看,G1也是追求短停顿时间的。

    G1收集器与上述的6种收集器相比,具有以下的特点:

 

  1. 并行和并发:G1收集器不仅能充分利用多CPU、多核环境下的硬件优势来减少停顿时间,而且仍可以通过并发的方式让用户线程继续执行
  2. 分代收集:虽然在使用G1收集器的时候,JAVA堆的内存布局已经不再是物理隔离了,仅仅是逻辑隔离,但是分代的概念得到保留,G1可以独立管理整个GC堆
  3. 空间整合:CMS采用了“标记-清除”算法,会产生内存碎片。而G1整体看来采用的是“标记-整理”算法,局部看来采用的是“复制”算法,故而G1运行期间都不会产生内存碎片,这种特性有利于程序长时间的运行。
  4. 可预测停顿:这是G1相比较CMS的一大优势,G1除了和CMS一样追求低停顿外,还能建立可预测的停顿模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒,这几乎已经是实时JAVA(RTSJ)垃圾收集器的特征了。

 

 

    G1最大的特点在我看来就是G1将Java堆划分成多个大小相等的独立区域(Region),虽然保留了新生代和老年代的概念,但是它们已经不再是物理隔离了,而都是一部分Region的集合。G1在后台维护一个 优先列表,这个列表中保存了G1收集到的各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收需要的时间的经验值),当需要进行垃圾回收的时候,根据用户允许的收集时间,优先回收列表中价值最大的那个Region(这就是Garbage-First,G1名字的由来)。这种使用Region划分空间以及根据优先级的区域回收方式,保证了G1收集器可以在有效的时间内获得尽可能高的收集效率,同时也避免了在整个JAVA堆中进行全区域的垃圾收集。

    G1产生的原因我认为就是HotSpot团队对于低延时和吞吐量两者同时考虑,不断追求一个完美的垃圾收集器的产物,虽然在执行流程上和CMS有差不多,并且在初始标记和最终标记阶段都需要暂停用户线程,但是通过重新定义JAVA堆,引出了Region的概念,成功的让其在性能上能兼顾到低延时和吞吐量,且不需要依赖其他收集器。G1的未来就是优化初始标记和最终标记阶段,如果能解决这两个阶段的用户线程暂停,实现并发,那么就很有可能产生一个近似理想状态的一个垃圾收集器。

    不过用户对于新生事物必须的认同需要一定的时间,并且之前垃圾收集器相互配合也可以满足用户需求,那么G1对于大多数公司来说不是必需品,但是,随着技术的不断成熟,我认为G1很有可能成为Server模式下的HotSpot默认的收集器。

    对于G1收集器,可以参考:http://f.dataguru.cn/thread-514678-1-1.html

技术分享

 

【5】JVM-垃圾收集器