(六)垃圾收集器

1.概述
在我们开展关于HotSpot虚拟机收集器讨论之前,我们来简单回顾一下JVM堆和方法区内存区域的划分与管理以及针对不同区域所采用的垃圾回收算法。

JVM堆与方法区内存区域.png

从上图我们可以看出,堆内存管理采用分代管理最为合适,Why?因为不同对象的生命周期不同,而且98%的对象都是新生代中的临时对象。而且,根据各代的特点应用不同的GC算法,提高GC效率。

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。这里讨论的收集器基于JDK1.7-Update14之后的HotSpot虚拟机,其包含的所有收集器如下图所示。


HotSpot虚拟机的垃圾收集器.png

从上图中,我们看不到针对永久代的垃圾回收器,那么永久代的垃圾该如何处理呢?当永久代和老年代触发GC时,除CMS外均会触发Full GC。首先按照新生代配置的GC方式进行Minor GC,再按照老年代配置的GC方式对老年代和永久代进行GC。若JVM估计Minor GC后可能会发生晋升失败,则直接采用老年代配置的GC方式对新生代、老年代和永久代进行Full GC。

2.新生代可用GC
新生代可用的垃圾收集器有:Serial收集器、ParNew收集器、Parallel Scavenge收集器和G1收集器。其中,Serial收集器、ParNew收集器和Parallel Scavenge收集器三者的共同点如下:

  • 均使用复制算法,原理上是一致的:
    a.拷贝Eden和From中存活对象到To中;
    b.部分对象由于某些原因晋升到Old中;
    c.清空Eden、From,From和To交换身份直到下一次GC发生。
  • 分配对象内存时,Eden空间不足时触发GC。

2.1.Serial收集器(串行收集器)
Serial收集器是最基本、发展历史最悠久的收集器,它是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop The World),直到它收集结束

Serial和SerialOld收集器运行示意图.png
  • 特性:Serial(串行)、Stop The World(暂停所有的工作线程)
  • 适用场景:单CPU、新生代小、对暂停时间要求不高的应用,如Client模式的应用
  • 对象直接分配在Old(老年代)的情况:对象大小超过Eden空间大小和大对象
  • 对象晋升规则:经历多次MinorGC仍存活的对象;To空间不足对象直接晋升

2.2.ParNew收集器(并行收集器)
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。

ParNew和SerialOld收集器运行示意图.png
  • 特性:Parallel(并行)、Stop The World(暂停所有的工作线程)
  • 适用场景:多CPU、Server模式
  • 可以搭配Serial Old和CMS垃圾收集器,不可搭配Parallel Old垃圾收集器

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境都不能百分百保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。ParNew收集器默认开启的GC线程数与CPU的数量相同,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数

2.3.Parallel Scavenge收集器(并行清除收集器)
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器······看上去和ParNew都一样,那它有什么特别之处呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的则是达到一个可控制的吞吐量(Throughput),所以也称为“吞吐量优先”收集器

Parallel Scavenge与Parallel Old收集器运行示意图.png

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。Parallel Scavenge收集器还有一个参数-XX:+useAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor去的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量,这种调节方式称为GC自适应的调节策略

  • 特性
    a.Parallel(并行)、Stop The World(暂停所有的工作线程)
    b.并行线程数默认值:
    CPU<=8:等于CPU核数;CPU>8:(3+CPU核数*5)/8;指定-XX:ParallelGCThreads=4
    c.会根据MinorGC的频率、时间等动态调整Eden/S0/S1的大小,可通过参数-XX:+useAdaptiveSizePolicy取消这一特性
  • 适用场景:多CPU、Server级别(2核2G内存)机器、吞吐量要求高
  • 对象直接分配在Old(老年代)的情况:
    a.在TLAB和Eden上分配失败,且对象大于Eden的一般大小
    b.晋升老年代对象年龄(-XX:PretenureSizeThreshold)参数是无效的
  • 对象晋升规则:经历多次MinorGC仍存活的对象;To空间不足对象直接晋升

3.老年代可用GC
3.1.Serial Old收集器(串行老年代收集器)
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。使用“标记-整理”算法,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,有两大用途:一种用途是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一个用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Serial和SerialOld收集器运行示意图.png
  • 特性:Serial(串行)、Stop The World(暂停所有的工作线程)
  • 算法:“标记-整理”算法
  • 场景:Client模式下使用、与Parallel Scavenge收集器搭配使用和CMS收集器的预备方案

3.2.Parallel Old收集器(并行老年代收集器)
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge和Parallel Old收集器的组合。

Parallel Scavenge与Parallel Old收集器运行示意图.png
  • 特性:Parallel(并行)、Stop The World(暂停所有的工作线程)
  • 算法:“标记-整理”算法
  • 场景:注重吞吐量以及CPU资源敏感的场合

3.3.CMS收集器(并发标记清除收集器)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

CMS收集器运行示意图.png

CMS收集器是基于“标记-清除”算法实现的,它的运作过程分为4个步骤:

  • 初始标记(Initial Mark):标记GC Roots能直接关联到的对象,耗时短
  • 并发标记(Concurrent Mark):从GC Roots出发,并发地标记可达对象
  • 重新标记(Remark):修正并发标记期间因程序运行而重新关联到GC Roots的对象标记
  • 并发清除(Concurrent Sweep): 并行地进行无用对象的回收

CMS是一款优秀的收集器,但是CMS还远不达到完美的程度,它有3个明显的缺点:

  • CMS收集器对CPU资源非常敏感:CMS默认启动的回收线程数是(CPU数量 + 3)/ 4,即当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时,CMS对用户程序的影响就会很大,降低了程序的响应速度。
  • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
    a.并发清除阶段用户线程不断运行就会产生新的垃圾,只能等下一次GC时被收集;
    b.并发清除阶段需要给与用户线程预留足够的内存空间运行,不能等到老年代被填满了再进行回收。可设置参数:-XX:CMSInitiatingOccupancyFraction=68指定当老年代使用了68%的内存空间后就会触发Full GC。在JDK1.6中,CMS收集器的启动阈值已提升至92%,要是CMS运行期间预留的内存无法满足程序需要就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动预备方案:临时启用Serial Old收集器来重新进行老年代的垃圾收集。
  • 内存碎片:CMS收集器是基于“标记-清除”算法实现的,这意味着收集结束后会有大量的空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
    a.-XX:+UseCMSCompactAtFullCollection:每次Full GC后进行内存压缩
    b.-XX:CMSFullGCsBeforeCompaction:多少次Full GC后进行一次内存压缩

4.G1收集器(Garbage-First收集器)
G1收集器是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1收集器具备以下特点:

  • 并行与并发:G1收集器能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要暂停Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。
  • 空间整合:与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存,有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿时间外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒。

在G1之前的其他收集器进行收集的范围都是整个新生代或老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但它们已不再是物理隔离的了,它们都是一部分Region的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
把Java堆分为多个Region后,垃圾收集是否真的就能以Region为单位进行了?听起来顺理成章,再仔细想想就会发现问题所在:Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才有,只是G1中更加突出而已。在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下几个步骤:

  • 初始标记(Initial Mark):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Mark):是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Mark):是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的哪一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这个阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
G1收集器运行示意图.png

5.理解GC日志
阅读GC日志是处理Java虚拟机内存问题的基础技能,GC日志只是一些人为确定的规则,没有太多技术含量。每一种收集器的日志形式都是由它们自身的实现所决定的,但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性。例如一下两段典型的GC日志:
33.125:[GC [DefNew:3324K->152k(3712k),0.0025925 secs] 3324K->152K(11904K) 0.0031680 secs]
100.667:[Full GC [Tenured:0K->210K(10240K),0.0149142 secs] 4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs] [Times:user=0.01 sys=0.00,real=0.02 secs]

  • 最前面的数字“33.125”和“100.667”:代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
  • GC日志开头的“[GC”和“[Full GC”说明了这次GC的停顿类型。如果有“Full”说明这次GC是发生了Stop-The-World的。
  • 接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集器是密切相关的。DefNew:是Serial收集器中新生代的名称缩写(Default New Generation),ParNew:是ParNew收集器中新生代的名称缩写(Parallel New Generation),PSYoungGen:是Parallel Scavenge收集器中新生代名称的缩写。
  • 后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”
  • 再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。有的收集器会给出更具体的时间数据,如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,这里面的user、sys和real与Linux的time命令所输出的时间含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU时间和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。CPU时间和墙钟时间的区别是:墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程时间,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以看到user或sys时间超过real时间是完全正常的。

6.垃圾收集器参数总结

参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,设置后,使用Serial+Serial Old的收集器组合进行内存回收
UseParNewGC 设置后,使用ParNew和Serial Old两种组合的垃圾收集器进行GC
UseConcMarkSweepGC 设置后,使用ParNew、CMS和Serial Old组合的垃圾收集器进行GC
UseParallelGC 虚拟机运行在Server模式下的默认值,设置后,使用Parallel Scavenge 和Serial Old的组合垃圾收集器进行GC
SurvivorRatio 年轻代中Eden Space和Survivor Space区域的容量比值,默认为8,即Eden:Survivor=8:1
PretenureSizeThreshold 代表直接进入老年代中的对象大小,设置此值后,大于这个参数的对象将直接在老年代中进行内存分配
MaxTenuringThreshold 在分代GC算法中,此值代表对象转移到年老代中的年龄,每个对象经历过一次年轻代GC(Minor GC)后,年龄就加1,到超过设置的值后,对象转移到老年代
UseAdaptiveSizePolicy 动态调整Java堆中各个区域的大小以及进行年老代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代中的剩余内存空间不足以应付年轻代的整个Eden和Survivor Space的所有对象都存活的极端情况
ParallelGCThreads 设置并行GC时进行内存回收的线程数量
GCTimeRatio GC时间占总时间的比率,默认值是99, 即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效
MaxGCPauseMillis 设置GC的最大停顿时间。仅在使用Parallel Scavenge收集器时生效
CMSInitiatingOccupancyFraction 设置CMS收集器在老年代时间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效
UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在使用CMS收集器时生效
CMSFullGCsBeforeCompaction 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用CMS收集器时生效

7.内存分配与回收策略
Java技术体系所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存,接下来我们来介绍几条最普通的内存分配规则。

  • 对象优先在Eden分配
    大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。
  • 大对象直接进入老年代
    所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
  • 长期存活的对象将进入老年代
    虚拟机提供了一个-XX:MaxTenuringThreshold参数设置对象晋升老年代的年龄阈值。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加一,当它的年龄增加到一定程度(默认为15岁)就将会被晋升到老年代中。
  • 动态对象年龄判定
    为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保
    在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,将尝试着进行一次Minor GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这是也要改为进行一次Full GC。

你可能感兴趣的:((六)垃圾收集器)