2-4 垃圾收集器与内存分配策略 - 垃圾收集器

垃圾收集器

简述:如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。java规范中没有对垃圾收集如何实现进行任何规范,所以不同厂家、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别。

这里讨论的是JDK1.7Update14之后版本的HotSpot虚拟机所包含的垃圾收集器。如下图:
HotSpot中的收集器

上图中新生代收集器主要有Serial、ParNew和ParallelScavenge。老年代收集器主要有CMS、SerialOld(MSC)和ParallelOld。G1收集器不分代,它既可以收集新生代也可以收集老年代。如果收集器之间有连线,那么说明两个收集器可以搭配使用。

在接下来我们将逐一介绍这些收集器的特性、基本原理和应用场景。在介绍的过程中会对收集器进行一些对比,但没有要决出雌雄的意思,只是希望通过了解各收集器的优缺点来更清楚收集器的使用场景,因为到目前为止也没有一种收集器可以做到称霸天下。


一、Serial收集器

简介:Serial收集器是最基本、发展历史最悠久的收集器。它是一个单线程收集器,并且当Serial收集器工作时,它会停止应用的所有工作线程(Stop The World),直到Serial收集完成。Serial属于串行收集器。

Serial和SerialOld收集器配合使用的工作流程示意图如下:
Serial/SerialOld流程图

对于很多应用来讲,在用户不可见的情况下将所有正常工作的线程全部停掉是难以接受的,但确不得不停止(上篇《垃圾收集算法实现》https://blog.csdn.net/sanbowla/article/details/81363373一文中对Stop The World的必要性有详细讲解)。但实现垃圾收集是一个复杂的过程,从JDK1.3开始,HotSpot虚拟机开发团队都在努力消除或减少因内存回收而导致工作线程停止的间隔。

上面对Serial收集器的介绍有点消极,实际Serial收集器的优点也很突出,简单而高效(相对于其他收集器的单线程模式)!在限定了单个CPU的环境,Serial收集器没有线程交互的开销,专心做垃圾收集,自然可以获得最高的单线程收集效率。到目前为止,Serial收集器依然是Client模式下新生代的默认收集器。

Serial收集器的控制参数包括

  • -XX:SurvivorRatio 设置Edon和Survivor空间比 -XX:SurvivorRatio=8 Edon是Survivor的8倍也就是 8:1:1(回翻博文《垃圾收集算法》https://blog.csdn.net/sanbowla/article/details/81035280中有Edon和Survivor以及垃圾收集复制算法的详解)
  • -XX:PretenureSizeThreshold 该命令是让大于这个设置值的对象直接在老年代中分配内存(超长的字符串,超大的数据等)
  • -XX:HandlePromotionFailure 该命令是配置是否允许分配担保失败。
    当新生代进行GC(MinorGC)时,虚拟机会检测老年代是否能够申请到足够大的连续空间来存储新生代所有对象,如果可以,那么直接触发MinorGC。如果小于,虚拟机会检测HandlePromotionFailure参数是否允许分配担保失败,如果允许,那么会继续检测之前历次分配担保到老年代对象的平均大小是否小于当前老年代可以申请到的最大空间,如果小于,触发MinorGC(此操作存在风险),如果大于或者HandlePromotionFilure设置不允许,那么直接触发全堆内存回收(FullGC)
    备注:MajorGC为老年代GC

二、ParNew收集器

ParNew收集器实际就是Serial收集器的多线程版本,除了使用多条线程进行垃圾回收外,ParNew收集器的所有行为都与Serial相同。包括控制参数、收集算法、Stop The World、对象分配规则、回收策略等都完全一样。

ParNew和Serial Old收集器工作流程图如下:
这里写图片描述
ParNew收集器是运行在Server环境下的首选收集器,一个很重要的与性能无关的原因是除了Serial收集器外只有ParNew才能和老年代的CMS收集器配合使用(下面详细介绍CMS收集器)。

  • -XX:+UseConcMarkSweepGC 此命令意思是选用CMS收集器,默认新生代会选用ParNew收集器

  • -XX:+UseParNewGc 强制新生代使用ParNew收集器

单CPU环境ParNew收集器的效果会弱于Serial收集器,ParNew收集器需要付出线程交互的代价;双CPU也不完全能超越Serial收集器。但多CPU环境ParNew更能有效利用系统资源。ParNew默认开启的收集线程数与CPU数量相同,可以通过-XX:ParallelGCThreads参数来限制垃圾收集的线程数。


三、Parallel Scavenge收集器

简述:Parallel Scavenge属于并行的多线程新生代收集器,与其他收集器不同的是,Parallel Scvenge收集器的关注点不是尽可能的缩短GC停顿,而是达到一个可控制的吞吐量(Throughput)。

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗事件的比值。

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
即:虚拟机总共运行100秒,垃圾收集花掉1秒,那么吞吐量就是99%。

关注点是吞吐量或缩短停顿时间适用于不同的应用场景。

  • 停顿时间越少就越适合需要与用户交互的界面,良好的响应速度能够提升用户体验。
  • 高吞吐量可以更加高效的利用CPU资源,尽快完成程序的运算任务,主要适合后台运算程序,不需要太多交互任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是-XX:MaxGCPauseMillis(垃圾收集最大停顿时间)参数和-XX:GCTimeRatio(吞吐量大小)参数。

  • -XX:MaxGCPauseMillis 的参数是一个大于0的毫秒数,收集器会尽量将垃圾收集时间控制在这个时间数值以内。但此参数绝对不是设置的越小越好。当此参数变小时,系统会相应的将新生代空间减少,收集300M一定比收集500M来得快。空间的缩减会直接导致垃圾收集操作更加频繁。原来10秒收集一次,停顿100毫秒,现在变成5秒收集一次,停顿70毫秒。虽然停顿时间减少了,但同时系统的吞吐量也相应的下降了
  • -XX:GCTimeRatio 的参数是一个大于0并且小于100的参数,是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。如果将此参数设置为19,那么GC时间占比为5%(1/(1+19)),默认为99,GC时间占比为1%(1/(1+99))。
  • -XX:AdaptiveSizePolicy 是一个开关参数,当这个参数打开后,就不用手工设置新生代大小(-Xmn)、Eden与Survivor区比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了。虚拟机会根据检测到的系统信息动态的配置参数已达到合适的停顿时间和吞吐量。只需要配置好最大堆(-Xmx)和优化目标(MaxGCPauseMillis或者GCTimeRatio)即可。自适应策略也是Parallel Scavenge和ParNew虚拟机的最大区别。

四、Serial Old收集器

简述:Serial Old收集器是Serial收集器的老年代版本,同Serial收集器一样,Serial Old收集器也是单线程收集器,应用于Client模式下的老年代GC回收。

它主要有两大用途,一是在JDK1.5以及之前的版本中与Parallel Scavenge收集器配合使用(实际Parallel Scavenge收集器框架中有PS MarkSweep收集器来进行老年代收集,但PS MarkSweep与Serial Old的实现非常相似,所以在官方的许多资料中都用Serial Old代替PS MarkSweep进行讲解),二是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。


五、Parallel Old收集器

简述:Paralle Old收集器是从JDK1.6版本才开始提供的,它是Parallel Scanvenge的老年代版本,使用多线程和标记-整理算法。

在Parallel Old收集器诞生之前,Parallel Scavenge收集器一直处于很尴尬的位置,如果新生代选择Parallel Scavenge收集器,那么老年代只有Serial Old收集器可以配合使用。由于Serial Old在服务器端应用性能上的拖累,使用了Parallel Scavenge收集器也不一定获得吞吐量最大化的效果。直到Parallel Old收集器的出现,才成为名副其实的吞吐量优先的收集器组合。

Parallel组合适用于注重吞吐量和CPU资源敏感的场合。工作流程图如下:
这里写图片描述


六、CMS收集器

简述:CMS(Concurrent Mark Sweep)收集器是以最短垃圾收集时间为目标的收集器,用以达到更好的服务响应速度。适用于互联网站和B/S系统等对响应速度有要求的服务端上。

CMS收集器是基于标记-清理算法来实现的,主要由下面4个步骤组成:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

1.初始标记与重新标记两个步骤仍然需要 Stop The World。初始标记只需要标记下与GC Roots可以直接关联到的对象(速度很快)。

2.并发标记是进行GC Roots Tracing的过程,追踪所有对象,并标记可达对象(耗时较长)。

3.重新标记需要Stop The World,是为了修正并发标记时,由于用户线程运行而导致标记产生变化的情况(比初始标记时间略长)。

4.并发清除就是字面意思,GC线程与用户线程同时运行(耗时较长)。

流程图如下
这里写图片描述

由于并发标记和并发清理这两个耗时最长的过程可以与用户线程并发进行,初始标记和重新标记消耗的时间很短暂,所以我们可以说CMS收集器的内存回收过程是和用户线程并发进行的。但CMS收集器还远达不到完美的程度,它主要有以下三个明显缺点:

  1. CMS收集器对CPU资源非常敏感(所有面向并发设计的程序都对资源很敏感)。在并发阶段,GC执行会占用一部分CPU资源(CMS收集器默认开启的垃圾收集线程数为(CPU数 + 3)/4),在CPU数量大于4时,GC至少占用25%的CPU资源。当仅有两个CPU时,GC会占用50%的CPU资源,程序运行速度减掉一半,细思极恐。为了应对这一情况,虚拟机提供了一种i-CMS增量式并发收集器(如果感兴趣的朋友可以百度一下),但实践效果很一般,目前i-CMS已被声明为不提倡用户使用了。
  2. CMS无法处理浮动式垃圾(Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。由于并发清理时用户线程还在运行,那么新的垃圾就会不断产生,这些新的垃圾就是浮动式垃圾,CMS无法清理掉这部分垃圾,只能等到下一次GC。由于清理动作和用户线程同时工作,就导致CMS不可以像其他垃圾收集器一样等到老年代几乎被填满时才去执行GC操作,它必须为并发运行的用户线程预留出足够的空间。
    可以通过命令-XX:CMSInitiatingOccupancyFraction来设置触发回收的百分比。JDK1.5默认为68%,JDK1.6被提高到92%。如果设置触发比过高,老年代没有预留足够的内存供用户线程使用,会产生Concurrent Mode Failure失败,这时虚拟机会启动Serial Old为应急预案负责回收老年代,停顿时间会倍增。所以如果触发比设置过高反而会导致性能降低。
  3. CMS是基于标记-清理算法实现的收集器,那也就意味着每次GC结束都会有大量内存碎片,内存碎片过多会导致老年代的剩余内存还很多,但确无法申请到足够连续的内存空间,从而不得不提前触发Full GC。
    可以通过命令-XX:UseCMSCompactAtFullCollection来解决此问题(默认开启),命令的功能是在即将触发Full GC之前对老年代进行一次碎片合并整理(内存压缩),但内存整理的过程必须要Stop The World,停顿时间被迫变长。
    我们还可以通过-XX:CMSFullGCsBeforeCompaction来设置多少次不压缩的Full GC后进行一次带压缩的Full GC(默认值为0,每次都会压缩)。

七、G1收集器

简述:G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器,是当今收集器技术发展的最前沿成果之一。

与其他的垃圾收集器相比,G1收集器的主要特点如下:

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿

1.G1收集器可以充分利用多CPU或多核的硬件优势来缩短Stop The World,在其他部分虚拟机中需要Stop The World来执行的GC线程在G1中可以通过并发的方式与用户线程同时进行。

2.G1收集器不需要与其它收集器配合就可以对新生代和老年代进行GC,它是采用不同的方式来处理新创建的对象和已经存活过一段时间熬过多次GC的旧对象来获得更好的收集效果。

3.G1收集器从整体看是基于标记-整理算法实现的,而从局部(两个region之间)看,是基于复制算法实现的。不过无论基于哪一种算法,GC过后也不会出现空间碎片,可以提供规整的可用内存,这种特性有利于程序长期运行。

4.同CMS收集器一样,G1收集器一样致力于缩短GC停顿。但G1收集器优于CMS的特点是它可以建立一个可预测的停顿时间模型,用户可以设置在M毫秒的时间片段内,GC停顿不能超过N秒。这几乎已经是实时JAVA(RTSJ)收集器了。

G1之前的收集器都是针对整个青年代或者老年代的,而G1收集器却是针对整个堆内存的,因此采用G1收集器会使JAVA堆内存布局产生变化,将堆内存划分成多个大小相等的独立区域(Region),青年代和老年代不在是物理隔阂,它们都是一部分Region的集合。

再说G1为什么可以建立可预测的停顿时间模型,由于Region概念的引入,G1可以有计划地避免全堆扫描。G1会在后台维护一个优先列表,表中记录着每个Region中垃圾的堆积情况以及清理所需要的时间经验,G1会根据表中记录的清理所需时间计算出在用户设置的最大GC时间内可以清理的Region集合(优先回收价值最大的Region),达到在有限的时间内以更加高效的方式回收垃圾。

基于分代收集我们设想这样一个问题,分别存于新生代和老年代的对象之间也会发生引用关系,那么在对新生代进行内存回收时就不得不同时扫描老年代,这样肯定会对Minor GC的效率有很大影响。同样,在G1中虽然将堆分为若干个Region空间,但各个Region内存储的对象之间也会发生引用关系,这样实际在对某个Region进行垃圾回收时也不得不扫描堆中的每个Region空间,如果是这样那完全没有达到预想的分块收集的效率。

在分代收集器与G1收集器中,虚拟机使用Remembered Set来避免全堆扫描。在G1中,每一个Region都对应一个Remembered Set集合。当两个对象要建立引用时,虚拟机会产生一个Write Barrier来暂时中断操作,并检查两个对象是否分属不同Region,如果分属不同Region,会在被引用对象所属Region对应的Remembered Set中添加一个记录引用信息的CardTable。这样,在GC扫描时,只需要将Remembered Set也作为GC Roots就可以避免全堆扫描。

这里写图片描述

上图是G1收集器的工作流程图,从图中我们可以看到G1收集器的运作可以大致分为4个步骤:

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)

从流程图中我们可以看到,G1的初始标记与并发标记阶段与CMS基本一致,但在并发标记阶段G1收集器会将这个阶段产生变化的引用记录到Remembered Set Logs中,然后在最终标记阶段只需要将Remembered Set Logs和Remembered Set进行合并即可。筛选回收阶段会在用户控制的时间范围内尽可能多的回收Region空间。

如果你的程序追求低停顿或停顿可控,可以选择G1收集器,如果追求吞吐量,那么使用G1不会带来好处。

感谢阅读:

由于个人水平有限,如果有不对的地方希望各位能够留言指正,谢谢!

参考书籍:

《深入理解java虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值