JVM垃圾收集器(八)

一、垃圾收集器

有了前面JVM参数的了解下面来看下JVM的垃圾收集器;如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。 

 JVM(HotSpot)有7种垃圾收集器,7种垃圾收集器作用于不同的分代,如果两个收集器之间存在连续,就说明他们可以搭配使用。从JDK1.3到现在,从Serial收集器-》Parallel收集器-》CMS-》G1,用户线程停顿时间不断缩短,但仍然无法完全消除。

1.1、Serial收集器(串行收集器)

  Serial收集器是一个单线程的收集器。“单线程”的意义不仅仅是它只会使用一个CPU或一条收集器线程去完成垃圾收集工作,更重要的是它在垃圾收集的时候,必须暂停其他所有工作的线程,直到它收集结束。

  Serial收集器是HotSpot虚拟机运行在Client模式下的默认新生代收集器。

  Serial收集器具有简单而高效,由于没有线程交互的开销,可以获得最高的单线程收集效率(在单个CPU环境中)。

  ”-XX:+UseSerialGC”:添加该参数来显式的使用Serial垃圾收集器。

1.1.1、优缺点

优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器

1.2、ParNew收集器

 

 

 

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

  ParNew收集器是许多运行在Server模式下的虚拟机首选的新生代收集器,其中一个原因是,除了Serial收集器之外,目前只有ParNew收集器能与CMS收集器配合工作。 

  ”-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器。

       “-XX:+UseParNewGC”:强制指定使用ParNew。   

       “-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同。

  并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

  并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能是交替执行),用户线程继续工作,而垃圾收集程序运行在另一个CPU上。

 1.2.1、优缺点

优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器

1.3、Parallel Scavenge收集器

       Parallel Scavenge收集器是一个新生代收集器,使用复制算法,且是并行的多线程收集器。

  Parallel Scavenge收集器关注点是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)),而其他收集器关注点在尽可能的缩短垃圾收集时用户线程的停顿时间。

  Parallel Scavenge收集器提供了两个参数来用于精确控制吞吐量,一是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数,二是控制吞吐量大小的 -XX:GCTimeRatio参数;

  “ -XX:MaxGCPauseMillis” 参数允许的值是一个大于0的毫秒数,收集器将尽可能的保证内存垃圾回收花费的时间不超过设定的值(但是,并不是越小越好,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,如果设置的值太小,将会导致频繁GC,这样虽然GC停顿时间下来了,但是吞吐量也下来了)。

  “ -XX:GCTimeRatio”参数的值是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,默认值是99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

  “-XX:UseAdaptiveSizePolicy”参数是一个开发,如果这个参数打开之后,虚拟机会根据当前系统运行情况收集监控信息,动态调整新生代的比例、老年大大小等细节参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略。

1.4、Serial Old收集器

  Serial Old收集器是Seria收集器的老年代版本,他同样是一个单线程收集器,使用” 标记-整理” 算法,运行过程和Serial收集器一样。

  Serial Old收集器主要用于Client模式下的虚拟机使用。

  Server模式下的两大用途:一、在JDK1.5及之前的版本与Parallel Scavenge收集器搭配使用;二、作为CMS收集器的后备方案,在并发收集发生Conturrent Mode Failure时使用。

1.5、Paraller Old收集器

       Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法;也是更加关注系统的吞吐量。 在JDK1.6中才出现。

1.6、CMS(Conturrent Mark Sweep)收集器

  CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

  目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上。

  CMS收集器是基于“标记-清除”算法实现,它的整个运行过程可以分为:初始登记(标记一下GC Roots能直接关联到的对象,这个过程速度很快)、并发标记(进行GCRoots Tracing的过程)、重新标记(修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,速度稍慢)、并发清除(清除死亡的对象)4个步骤;其中,初始标记和重新标记仍然需要“Stop The World”。

  CMS收集器运行的整个过程中,最耗费时间的并发标记和并发清楚过程收集器线程和用户线程是一起工作的,所以总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

1.6.1、优缺点

  优点:并发收集、低停顿。

  缺点:

    一:CMS收集器对CPU资源非常敏感。虽然在两个并发阶段不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是(CPU数量+3)/4。

    二:CMS收集器无法处理浮动垃圾,可能出现“Conturrent Mode Failure”失败而导致另一次Full GC产生。由于CMS并发清除阶段用户线程还在运行,伴随着程序还在产生新的垃圾,这一部分垃圾出现在标记之后,CMS无法在当次收集中处理掉它们,只能留到下次再清理,这一部分垃圾称为“浮动垃圾”。也正是由于在垃圾收集阶段用户线程还在运行,那么也就需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等待老年代填满之后再进行收集,需要预留一部分空间给并发收集时用户程序使用。可以通过“-XX:CMSInitiatingOccupancyFraction”参数设置老年代内存使用达到多少时启动收集。

    三:由于CMS收集器是一个基于“标记-清除”算法的收集器,那么意味着收集结束会产生大量碎片,有时候往往还有很多内存未使用,可是没有一块连续的空间来分配一个对象,导致不得不提前触发一次Full GC。CMS收集器提供了一个“-XX:UseCMSCompactAtFullCollection”参数(默认是开启的)用于在CMS收集器顶不住要FullGC时开启内存碎片整理(内存碎片整理意味着无法并发执行不得不停顿用户线程)。参数“-XX:CMSFullGCsBeforeCompaction”来设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值是0,意味着每次进入Full GC时都进行碎片整理)。

1.6.2、相关参数

//开启CMS垃圾收集器 
-XX:+UseConcMarkSweepGC
//默认开启,与-XX:CMSFullGCsBeforeCompaction配合使用
-XX:+UseCMSCompactAtFullCollection
//默认0 几次Full GC后开始整理
-XX:CMSFullGCsBeforeCompaction=0
//辅助CMSInitiatingOccupancyFraction的参数,不然CMSInitiatingOccupancyFraction只会使 用一次就恢复自动调整,也就是开启手动调整。
-XX:+UseCMSInitiatingOccupancyOnly
//取值0-100,按百分比回收
-XX:CMSInitiatingOccupancyFraction 默认-1
注意:CMS并发GC不是“full GC”。HotSpot VM里对concurrent collection和full collection有明确的区分。所有带有“FullCollection”字样的VM参数都是跟真正的full GC相关,而跟CMS并发GC无关的。 

1.7、G1(Garbage-First)收集器

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。每个Region大小都是一样的,可以是1M到32M之间的数值,根据总的内存大小而定,目标是数量不超过2048个,但是必须保证是2的n次幂;如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中;设置Region大小:-XX:G1HeapRegionSize=M;所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域

每个RegionG1中扮演了不同的角色,比如Eden(新生区),比如Survivor(幸存区),或者Old(老年代)

除了传统的老年代,新生代,G1还划分出了Humongous区域,用来存放巨大对象(humongous object,H-obj)。

对于巨大对象,值得注意的有以下几点:

  • H-obj的定义是大于等于Region一半的对象
  • H-obj直接分配到Old gen,防止频繁拷贝。但是H-obj的回收却不是在Mixed GC阶段,而是concurrent marking阶段中的clean up过程和full GC
    ❝ 这点一定注意,在调优过程中你会在GC日志中经常发现这句
    [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0029216 secs]
    疑惑点就在于为什么Humongous Allocation却是引发的yong gc
    原因便是在于为了通过yong gcinitial-mark开始进行concurrent marking,进而通过clean up回收大对象❞
    ❝ 如果想要查看G1日志的时候,为了方便快速达到GC的效果,你可能会直接分配一些大对象以便填满整个堆从而引发GC,但是如果光是大对象,你可能会发现GC日志中并没有Mixed GC,而是频繁的Yong GCConcurrent Marking,这便是原因❞
  • H-obj永远不会被移动,虽然G1的回收算法总体上看是基于标记-整理的,但是对于H-obj则永远不会移动,要么直接被回收,要么一直存在。因此H-obj可能会导致较大的内存碎片进而引起频繁的GC

1.7.1、G1收集器的特点

      G1是一款面向服务端应用的垃圾收集器,Sun(Oracle)赋予它的使命是(在比较长期的)未来可以替换掉JDK 5中发布的CMS(Concurrent Mark Sweep)收集器,与其他GC收集器相比,G1具备如下特点:
  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的“标记-清理”算法不同,G1从整体看来是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上看是基于“复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另外一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器特征了。

1.7.2、实现思路  

       在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
  G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内获可以获取尽可能高的收集效率。
  G1把内存“化整为零”的思路,理解起来似乎很容易理解,但其中的实现细节却远远没有现象中简单,否则也不会从04年Sun实验室发表第一篇G1的论文拖至今将近8年时间都还没有开发出G1的商用版。笔者举个一个细节为例:把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?听起来顺理成章,再仔细想想就很容易发现问题所在:Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保障准确性?这个问题其实并非在G1中才有,只是在G1中更加突出了而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象也面临过相同的问题,如果回收新生代时也不得不同时扫描老年代的话,Minor GC的效率可能下降不少。 
  在G1收集器中Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查引是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

1.7.3、运作过程

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

  • 初始标记(Initial Marking)                                            标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程 
  • 并发标记(Concurrent Marking)                                   从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
  • 最终标记(Final Marking)                                             修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程 
  • 筛选回收(Live Data Counting and Evacuation)           对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划

  对CMS收集器运作过程熟悉的读者,一定已经发现G1的前几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。通过图1可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段。

JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题,只能在64位的linux上使用,目前用得还比较少,有兴趣的话可以去官网自己学习看下。
Tags:
Exit mobile version