Java虚拟机04——垃圾收集器

·  阅读 548

主要介绍HotSpot虚拟机的垃圾收集器,这个虚拟机包含的所有收集器如图所示:

image.png

可以看到,收集器之间是可以搭配使用的。下面介绍这些收集器的特性、基本原理和使用场景。在介绍之前先明确一个观点:直到现在为止还没有最好的收集器出现,更加没有万能的收集器,选择的是对具体应用最合适的收集器。

串行收集器

串行收集器是最基本、发展历史最悠久的收集器。它们的特点就是单线程运行及独占式运行,因此会带来很不好的用户体验。虽然它的收集方式对程序的运行并不友好,但由于它的单线程执行特性,应用于单个CPU硬件平台的性能可以超过其他的并行或并发处理器。

Serial收集器

  • 在JDk1.3之前是新生代收集的唯一选择
  • 单线程,只会使用一个CPU去完成
  • 垃圾收集时,必须暂停其他工作线程,直到它收集结束。“Stop The World”
  • 虚拟机后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉
  • 到现在为止,是虚拟机运行在Client模式下的默认新生代收集器

通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。

Serial Old收集器

  • 是Serial收集器的老年代版本,是单线程收集器,采用“标记-整理算法”
  • 主要意义也是在于给Client模式下的虚拟机使用
  • 如果在Server模式下,那么它主要还有两大用途:
  •   在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;
  •   作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中详细讲解。

Serial与Serial Old工作过程如图:

image.png

要启用老年代串行收集器,可以尝试使用下面的参数:

  • -XX:+UseSerialGC:新生代和老年代都是用串行收集器;
  • -XX:+UseParNewGC:新生代使用ParNew收集器,老年代使用串行收集器;
  • -XX:+UseParallelGC:新生代使用Parallel GC收集器,老年代使用串行收集器。

并行收集器

并行收集器是多线程的收集器,在多核CPU下能够很好的提高收集性能。

ParNew收集器

ParNew收集器是Serial收集器的多线程版本。除了使用多条线程进行垃圾收集之外,其余行为如所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew收集器的工作过程如下图所示:

image.png

  • 运行在Server模式下的虚拟机中首选的新生代收集器,因为除了Serial收集器外,目前只有它能与CMS收集器配合工作
  • 由于存在线程交互的开销,ParNew收集器在单CPU环境下性能并没有单线程垃圾收集器性能好。
  • 可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。一般来说,当CPU数量小于8个时,宜设置为CPU数量;当CPU数量大于8个时,宜设置为3 + (( 5 * CPU_Count ) / 8 )。

开启ParNew收集器可以使用以下参数:

  • -XX:+UseParNewGC:新生代使用ParNew收集器,老年代使用串行收集器。
  • -XX:+UseConcMarkSweepGC:新生代使用ParNew收集器,老年代使用CMS收集器。

ParNew是并行的收集器,在这里介绍一下并行与并发的概念

  • 并行(Parallel):多条收集器线程并行工作,但此时用户线程仍然处于等待状态。(合作)
  • 并发(Concurrent):用户线程与垃圾收集器线程同时执行,不一定是并行执行,可能是交替执行(竞争)

Parallel Scavenge收集器

Parallel Scavenge收集器与ParNew收集器类似,也是使用复制算法的并行的多线程新生代收集器。但Parallel Scavenge收集器关注可控制的吞吐量(Throughput)

注:吞吐量是指CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /( 运行用户代码时间 + 垃圾收集时间 )

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:

  • -XX:MaxGCPauseMillis:最大垃圾收集停顿时间,是一个大于0的毫秒数,收集器将回收时间尽量控制在这个设定值之内;但需要注意的是在同样的情况下,回收时间与回收次数是成反比的,回收时间越小,相应的回收次数就会增多。所以这个值并不是越小越好。
  • -XX:GCTimeRatio:吞吐量大小,是一个(0, 100)之间的整数,表示垃圾收集时间占总时间的比率。

除上述两个参数之外,Parallel Scavenge收集器还提供了一个参数-XX:+UseAdaptiveSizePolicy,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的。

由于如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(Parallel Scavenge无法与CMS收集器配合工作),Parallel Old收集器的出现就是为了解决这个问题。Parallel Scavenge和Parallel Old收集器的组合更适用于注重吞吐量以及CPU资源敏感的场合。Parallel Old收集器的工作过程下图所示:

image.png

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,也是基于“标记—清除”算法实现的,它的运作整个过程过程细分为4个步骤,包括:

  • 初始标记(CMS initial mark):需要“Stop The World”,标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,标记老年代所有存活对象。
  • 重新标记(CMS remark):需要“Stop The World”,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  • 并发清除(CMS concurrent sweep):清理垃圾。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行,如图:

image.png

CMS有以下3个明显的缺点:

  • CMS收集器对CPU资源非常敏感。CMS默认启动的回收线程数是( CPU Count + 3 ) / 4,当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,将分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%。

  • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。浮动垃圾是指CMS在并发清理阶段用户线程还在同时执行时产生的垃圾。由于在垃圾收集阶段用户线程还需要运行,还需要预留有足够的内存空间给用户线程使用,因此需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以降低内存回收次数;在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。如果运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将临时启用Serial Old收集器来重新进行老年代的垃圾收集,导致停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。

  • CMS是一款基于“标记—清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,因此也会导致停顿时间变长。而另外一个参数-XX:CMSFullGCsBeforeCompaction可以设置执行多少次不压缩的Full GC后,才执行一次带压缩的(默认值为0,即每次进入Full GC时都进行碎片整理)。

G1收集器

G1是一款面向服务端应用的垃圾收集器,与其他GC收集器相比,G1具备如下特点:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间。
  • 分代收集:分代概念在G1中依然保留。G1可以独立管理整个GC堆,且采用不同的方式去处理分代对象。
  • 空间整合:G1从整体来看是基于“标记—整理”算法实现的,从局部(两个Region之间)上来看是基于“复制”算法实现的;G1收集后能提供规整的可用内存。 可预测的停顿:G1能建立可预测的停顿时间模型,能明确指定垃圾收集相对于时间段的吞吐量。
  • G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1根据各个Region回收所获得的空间大小以及回收所需时间等指标在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1收集器运作步骤如下:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。(OopMap)
  • 并发标记(Concurrent Marking): 进行GC Roots Tracing的过程
  • 最终标记(Final Marking): 修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,也需要“Stop The World”。(修正Remebered Set)
  • 筛选回收(Live Data Counting and Evacuation): 首先对各个Region的回收价值和成本进行排行,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

image.png

垃圾收集参数总结

参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收
UseParNewGC 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用
UseParallelGC 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况
ParallelGCThreads 设置并行GC时进行内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认值为99,即允许 1% 的GC时间,仅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis 设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效
CMSInitiatingOccupancyFraction 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效
UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效
CMSFullGCsBeforeCompaction 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改