之前我们讲解了JVM的GC垃圾回收的算法与种类,本篇我们来介绍一下GC的各种收集器。通过运行参数的调整,我们可以设置GC的垃圾回收器类型,主要有串行收集器、并行收集器、CMS收集器。下面会一一给大家介绍。

一、堆的回顾
在介绍回收器之前,我们先回顾一下之前的堆的知识:
【Java虚拟机探究】8.GC参数
前面提到过,堆根据类型可以分为新生代和老年代。其中新生代就是上图中的eden、s0和s1区域,老年代就是上图中的tenured。其中新生代是存放着“年龄”比较小的对象,老年代存放着“年龄”比较久的对象或超大对象。
当一个对象被new(创建)出来后,绝大多数首先会进入eden区(有一些栈上分配的对象会分配在栈上,比较大的对象可能会被直接分配到老年代),在eden区进行垃圾回收后,被幸存下来的对象则被分配至survivor区,其中survivor区被分为survivor0和survivor1,简称s1和s0。s1和s0区域是大小相等,相互完全对称的区域,GC在这两块区域进行垃圾回收的时候使用的是“复制算法”,一块是待清理区域,一块是空白区域准备接收清理后的对象。而因为“复制算法”的机制,s1和s0每次只会使用一块,因此会浪费掉其中一块空间。

当一个新生代对象经历过若干次垃圾回收仍然没有被回收,此时它到达了一定的“年龄”后就会被认为是“老年对象”,就会被引入老年代。

下面是之前的一个针对堆的各个区域的一个划分详细图:
【Java虚拟机探究】8.GC参数

二、GC参数-串行收集器
串行收集器简称“SerialGC”,是JVM自带的垃圾收集器的一种,是GC中最古老,但是最稳定的收集器,其效率也是最高的。
但串行回收器也有一个缺点,就是在垃圾回收的时候可能会产生较长的停顿。这是因为串行回收器是单线程的,也就是仅使用一个线程进行回收操作,这在多核机器上没有办法去发挥它的性能。
在JVM中使用串行回收器时,添加以下参数:
-XX:+UseSerialGC
一旦启用了串行回收器,新生代和老年代都会使用串行回收器进行垃圾回收。在新生代中使用“复制算法”,老年代使用“标记-压缩算法”。
下图就是串行回收器的运行机制:
【Java虚拟机探究】8.GC参数
在应用程序中,应用线程可能会有许多个,一旦回收开始,应用程序线程全部暂停,由GC线程来接替,而在串行回收器中,GC线程只有一个。当GC完成之后,应用程序线程开始恢复执行。

下面是一个新生代和一个老年代使用串行回收器进行GC的垃圾回收日志:
【Java虚拟机探究】8.GC参数
可以在该日志中了解堆、永久区的情况,以及GC花费的时间。

三、GC参数-并行收集器
并行收集器有多种,其中有ParNew收集器和Parallel收集器。
(1)ParNew收集器
ParNew收集器简称“ParNewGC”,其中Par就是英文“parallel”的缩写,指的就是“并行”的意思。而其中的“New”代表的意思就是为新生代服务,只影响新生代的收集,不影响老年代的收集。
在JVM中使用串行回收器时,添加以下参数:
-XX:+UseParNewGC
一旦启用了ParNew收集器,新生代就会使用“并行”回收,而老年代依然使用“串行”回收。该回收器在新生代进行回收的时候依然使用了“复制算法”进行回收,与串行回收器相比,它使用的是多线程来处理,因此在多核机器的情况下,性能会比较好。当然,线程的数量我们也是可以控制的,使用“-XX:ParallelGCThreads”参数,就可以来指定线程的数量。
下图就是并行回收器的运行机制:
【Java虚拟机探究】8.GC参数
一旦回收开始,应用程序线程全部暂停,由GC线程来接替,而在并行回收器中,会有多个GC线程进行垃圾回收工作。当GC完成之后,应用程序线程开始恢复执行。
这里要注意一点,多线程垃圾回收并不一定比单线程回收快,这要看是不是在多核(CPU)的硬件上运行。如果实在多核的硬件上运行,只要设置一个合理的线程数量,那么它的处理效率是要比单线程GC回收快。而在单核(CPU)的硬件上运行,并行回收期虽然是多线程,但是单核CPU要为多个线程分配内存,其实和单线程的消耗比并不占优势。

下面是使用ParNew收集器进行垃圾回收时的日志:
【Java虚拟机探究】8.GC参数
我们看到“ParNew”的标志,就可以知道这里使用了ParNew并行收集器来回收。

(2)Parallel收集器
Parallel收集器类似上面的ParNew收集器,它也是一种并行收集器。它和ParNew收集器的区别就是,Parallel收集器更加关注垃圾回收过程中的“吞吐量”。
在JVM中使用串行回收器时,添加以下参数:
-XX:+UseParallelGC
一旦启用了Parallel收集器,表明在新生代使用Parallel并行收集器,老年代依然使用串行收集器。
而开启下面的配置:
-XX:+UseParallelOldGC
则表明在在新生代使用Parallel并行收集器,且在老年代也进行并行回收。可以理解为串行收集器在新生代和老年代的并行化。

下面是使用“-XX:+UseParallelOldGC”配置Parallel收集器,进行垃圾回收时的日志:
【Java虚拟机探究】8.GC参数
可以看到红色部分,新生代和老年代使用的回收器的类型。

(3)并行回收器参数
有关并行回收器,我们还有两个参数可以设置。
1.“-XX:MaxGCPauseMills”
可以设置最大停顿时间,即时进行垃圾回收时所花费的事件,同时也是应用程序停顿的最大时间。当设置了该参数,GC会尽力保证回收时间不超过设定时间。

2.“-XX:GCTimeRatio”
该参数可以设置垃圾收集的时间站总时间的比,它的取值为1-100,一般默认为99,即允许最大1%时间做GC。

上面两个参数我们可以认为分别设置了GC的时间和GC的吞吐量。

值得一提的是,这连个参数实际上是矛盾的,因为停顿时间和吞吐量不可能同时调优。因为当我们将GC的回收频率增加时,每次的回收时间自然会变短,而由于GC次数变多,导致整个应用程序的运行效率变低了。而将GC的回收频率减小时,每次的回收时间较高频率的回收自然会变长。GC频率降低了,对于应用程序整体而言,运行效率是上升的,但是每次GC的时间过长,也会出现效率问题。

所以,我们要了解应用程序的主要矛盾在哪里,来合理的分配GC的时间和吞吐量,因为我们不可能将两者同时提高,除非优化GC的回收算法。

三、CMS收集器
CMS即是“Concurrent Mark Sweep”(并发标记清除)的缩写,也说明CMS收集器是使用“并发标记清除”的机制来进行垃圾回收的。
CMS收集器使用的“标记-清除”算法来进行垃圾回收,“标记-清除”算法就是从根元素进行可达对象的标记,将没有被标记的垃圾对象清除。

之前将的“并行”回收器,与这个“并发”回收器的区别是什么呢?所谓的“并发”指的就是垃圾回收器可以和应用程序线程一起执行,即不再像“并行”、“串行”收集器一样,需要停止所有的应用程序线程来进行GC垃圾回收,而是几乎无需停止应用程序线程,垃圾回收线程和应用程序线程交替进行,形成一种同时进行的效果。

与“并行”、“串行”收集器相比,“并发”收集器停顿时间会很小(也会停顿,具体见运行机制),因为它可以和应用程序线程一起运行。但是在“并发”阶段,垃圾回收器可能会吃掉一大部分的应用程序的CPU内存分配,此时应用程序的资源会大大减小,此时程序的吞吐量也大幅度降低。

在JVM中使用CMS回收器时,添加以下参数:
-XX:+UseConcMarkSweepGC
该收集器是一个单纯的老年代收集器,不作用于新生代(新生代使用ParNew)。

CMS运行过程比较复杂,因为它要达到和应用程序线程一起运行的目的,所使用的算法或实现机制就会比较复杂。
CMS运行过程着重实现了标记的过程,可分为以下阶段:
(1)初始标记
由根对象开始进行标记,来标记根可以直接关联到的所有对象。整个过程速度是比较快的。

(2)并发标记(和用户线程一起)
主要标记过程,将系统中所有的对象进行标记(标记为可用对象和垃圾对象)。

(3)重新标记
在正式清理前,还需要做一个重新标记的操作。这是因为并发标记时,用户线程依然运行,因此在正式清理前,需要再做一次修正,防止在这个过程中有疏漏。
该标记动作是独占CPU的,因此会产生一次停顿。

(4)并发清除(和用户线程一起)
基于之前的所有标记结果,直接清理垃圾对象。

可以看到在关键的“初始标记”和“重新标记”点,还是会有停顿产生,因为必须确定一个时刻的标记情况,如果应用程序一直在运行,则没有办法最终确定对象的情况。但是比较之前的串行和并行,这里的停顿时间少了很多。

下图是CMS运行收集器在回收过程中的基本情况:
【Java虚拟机探究】8.GC参数
可以看,首先是有多个应用程序线程同时运行,在“初始标记”过程中,应用程序线程停止,CMS线程进行执行。之后进行并发标记,将系统中所有的对象进行标记(标记为可用对象和垃圾对象),此时CMS线程是和用户线程一起执行的。然后进行重新标记,应用程序线程停止,CMS线程进行执行。最后进行并发清除,基于之前的所有标记结果,直接清理垃圾对象,此时CMS线程是和用户线程一起执行的。最后进行并发重置,将回收后的对象或数据结构进行清空或重置,为下一次标记做准备。

下图是一个进行CMS回收的日志:
【Java虚拟机探究】8.GC参数
看到CMS标记,就表明使用了CMS回收器。而CMS回收器是专用于老年代的回收器,所以看到它也即是表明进行了老年代的回收。其中“initial-mark”是初始标记,“concurrent-mark”是并发标记,“remark”是重新标记,“concurrent-sweep”是并发清除,“concurrent-reset”是并发重置。

CMS收集器的特点:
1.尽可能降低停顿,但会影响系统整体吞吐量和性能。
比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半。

2.清理不彻底
因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理。

3.因为和用户线程一起运行,不能在空间快满时再清理
如果不幸内存预留空间不够,就会引起concurrent mode failure,可以使用-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值,即应用程序的内存占用多少时,来触发GC。

下图就是在并发清理的时候,去清理对象的过程中,应用程序不断的进行新的内存申请,导致垃圾回收的内存不够用,此时就会报concurrent mode failure异常:
【Java虚拟机探究】8.GC参数
解决这种问题的办法就是,当CMS收集器内存不够时导致收集失败,此时使用串行收集器,将应用程序线程全部停止后,再来执行。即让串行收集器作为CMS收集器的后备。

我们之前提到过,标记-清除和标记-压缩的不同是,标记-清除是清理了垃圾对象,保留了有用对象,而标记-压缩在其基础上,将可用对象进行了压缩,使其其中在堆空间的一处区域:
【Java虚拟机探究】8.GC参数
相对于标记-压缩算法而言,标记-清除算法就会在内存空间中产生一定量的碎片。但是标记清除算法是CMS收集器的首选,因为CMS收集器要和应用程序一起执行,如果选择了标记-压缩算法,它在清理的时候,需要移动可用对象的空间,而应用程序线程也在运行过程中,它有可能会找不到之前的可用对象(因为被移动了),因此很难和应用程序并发执行。如果想要和应用程序并发执行,那么它就要保证这些可用对象在应用程序执行的过程中位置不会发生改变。
虽然CMS收集器选择标记清除算法很适合并行运行回收,但是产生内存碎片的问题还是存在。此时JVM提供了一系列整理的操作:
“-XX:UseCMSCompactAtFullCollection”:会在一次Full GC后,进行一次碎片整理,而整理过程是独占CPU的,会引起停顿时间变长。

“-XX:+CMSFullGCsBeforeCompaction”:设置进行几次Full GC后,进行一次碎片整理。

“-XX:ParallelCMSThreads”:设定CMS的线程数量,一般约等于可用的CPU数量。

四、减轻GC压力
为了减轻GC的压力,我们要从以下三个方面来解决该问题:
(1)软件如何设计架构
(2)代码如何写
(3)堆空间如何分配

五、GC参数整理
目前为止,我们对堆、垃圾回收等做了很多讲解,这里将使用的参数设置进行一个回顾和整理:
-XX:+UseSerialGC:在新生代和老年代使用串行收集器

-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例

-XX:NewRatio:新生代和老年代的比

-XX:+UseParNewGC:在新生代使用并行收集器

-XX:+UseParallelGC :新生代使用并行回收收集器

-XX:+UseParallelOldGC:老年代使用并行回收收集器

-XX:ParallelGCThreads:设置用于垃圾回收的线程数

-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器

-XX:ParallelCMSThreads:设定CMS的线程数量

-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发

-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理

-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩

-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收

-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收

-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

转载请注明出处:https://blog.csdn.net/acmman/article/details/80723694

相关文章: