深入JVM内核(五)——GC参数之垃圾收集器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gududedabai/article/details/81176390

由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。

一、堆的回顾

前面有总结,一般堆分为两类:新生代和老年代,即:eden+s0+s1(新生代)与老年代;老年代存放的是老年对象,新生代存放的是新的对象;一般来说对象开始创建出来时绝大多数是一般放在eden区,但也有一些例外:如栈上分配,创建的对象放在栈上,还有一些比较大的对象,有可能会直接分配到老年代。因此除此之外大部分对象都是在新生代中产生的。如果在gc之后他能幸存,那么他会被放到幸存代也可以称之为s0,s1,也就是也可以叫from,to。不管叫什么,他们指的都是一个区域。s0和s1他们是大小相等,功能上也是完全对称的。前面讲的复制算法可知道,新生代使用gc回收就是复制算法并且要求s0,s1两块区域是对称的。因此它的缺点就是s0,s1空间浪费,因为每次只能使用其中一块区域进行gc回收。因此会浪费掉一个区间。当一个对象经过多次gc之后就变成的老年对象,那么他就会被存放在老年代。

二、GC参数 - 串行收集器

1、jvn自带的一种收集器,他是最古老但也是最稳定的。同时他从整体效率也是很高的,虽然后面会介绍一个优化的收集器,但是他们只是在某一方面特性的进行优化进而提高收集效率。但从整体来看效率不一定会有这个高。

但是他遇到最大可能会产生较长的停顿,因为他是单线程的,在回收时,他使用一个线程进行gc,在多核情况下,没法发挥很好的性能。

2、使用串行收集器:    -XX:+UseSerialGC    这个参数就启用了串行收集器。

     因此新生代、老年代都使用串行回收;那么他会在新生代复制算法,在使用老年代标记-压缩算法

串行收集器工作原理如下:

下面是串行收集器的日志输出:

当我们看到GC与时间,那么他就是串行收集器输出了,如果看到full输出代表老年代回收的先关堆的情况,以及永久区的情况等等都能看到。

三、GC参数 - 并行收集器

1、ParNew  简称par

     -XX:+UseParNewGC     从名字上看带new他是新生代收集器,因此他在新生代使用并行收集器;在老年代串行收集器。因此在使用这个参数后,新生代会使用并行回收,老年代使用串行回收,因此这个参数只影响新生代的回收。

2.、  Serial收集器新生代的并行版本

3、 并行收集器依然使用的 复制算法并且他是多线程的,因此需要多核支持。

 4、  -XX:ParallelGCThreads 可以通过这个参数来指定或者限制线程数量

5、工作原理如图:

但是:使用多线程并不是一定比单线程快,要看在什么cpu情况下,如果在多核cpu情况,多线程会很快,但是也要设置合适线程数量,在单核情况最好还是使用单线程这个算法。当使用这个 参数是,我们可以看到日志会打印ParNew这个信息,则可是知道当前在使用新生代的并行回收器。

2、Parallel收集器(另一种并行收集器,与ParNew  有一定区别,区别在于他更加关注于吞吐量的并行收集器

       类似ParNew

      他是在 新生代使用复制算法, 老年代使用 标记-压缩;可以看做串行收集器在新生代与老年代的并行化。他更加关注吞吐量。

      -XX:+UseParallelGC  在新生代使用这个参数来启用并行收集器。只启用这个新生代使用并行收集,而老年代依旧使用串行。

             使用Parallel收集器+ 老年代串行

     - XX:+UseParallelOldGC  如果要老年代也是用并行,则使用这个参数。

             使用Parallel收集器+ 并行老年代

3、工作原理:

这个日志打印就是使用了UseParallelOldGC 之后打印的Full gc日志。红字可以看到gc的类型。

4、关于并行gc还可以设置其他参数,如下:

-XX:MaxGCPauseMills   表示最大停顿时间,单位毫秒

        GC尽力保证回收时间不超过设定值,也是gc的时候应用程序暂停的多少时间,也就是 gc所占用的时间,一种目标值,gc会尽量不超过这个值。不一定就到这个时间,并不一定能保证就这个时间。

-XX:GCTimeRatio  就是垃圾回收所用的cpu时间占总时间之比。  也就是垃圾收集时间占总时间的比

     取值范围 0-100的取值范围;  默认99,即最大允许1%时间做GC,也就是一般我们希望gc时间占用应用程序时间最小,停顿最小,影响最小。

但是事实上这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优。也是因为可以看做垃圾gc的总量是一定的,负载是一定的,就需要那么多时间去做这么多事,也就是说我们可以安排什么时候去做gc,那么也就是增加gc的频率,频率多了自然垃圾就少了,对象也就少了。因此每一次gc的时间都是很少的。但是频繁的gc对系统的性能也就造成影响。就造成了停顿时间比较短,但整体性能并不高。反言之,减少gc,那么性能会有所提升,那么GCTimeRatio这个值就会增大,因此gc如果次数少了,会造成面对大量的对象,导致gc的时间比较长。这种情况下,系统停顿的时间相对也就比较长。因此这两参数,一个代表性能,一个代表吞吐量。因此这里就是这个gc收集器的主要矛盾,根据实际取中间值,或者有所取舍。

四、GC参数 – CMS收集器   (Concurrent Mark Sweep 也就是并发的标记清除)

CMS收集器 Concurrent Mark Sweep 并发标记清除   并发(并发指的事gc回收器与应用程序交替执行,一起执行)也就是说他可以跟着应用程序一起执行,所以他的停顿会比较少。同样,吞度量会降低。因为他会占用应用程序在cpu中的比率,占用一定资源从而降低了应用程序的吞吐量。

他使用了标记-清除算法 与标记-压缩相比 并发阶段会降低吞吐量

   这个收集器他是一个 老年代收集器(新生代使用ParNew)他不会再新生代中去使用,所以他是单纯的老年代收集器。

  使用该参数     -XX:+UseConcMarkSweepGC  启用CMS收集器

CMS运行过程比较复杂(复杂是因为他要尝试试图与应用程序一起执行,这也就是他的特点,为了达到这个目的所以他的算法与实现机制会比较复杂),着重实现了标记的过程。

主要工作可分为以下几步:

第一步、初始标记

标记那些就是由根对象 ,可以直接关联的的对象 (根可以直接关联到的对象)

这个速度是非常快的;但是他是要产生全局停顿的。在这个过程完成之后开始执行第二步。

第二步、并发标记(和用户线程一起,用户线程一边工作,他一边标记,将所有的对象进行标记,标记为是垃圾还是不是垃圾)

主要标记过程,标记全部对象,在这个并发标记过程中,用户线程依旧在运行,因此在清理之前,他还需要做一次修正。也就是重新标记。

第三步、重新标记  (可以看做并发标记的补充)

由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。重新标记依旧是独占cpu的,这时候也会产生停顿。之后就可判定标记是完善的了,就可以进行并发清除。

第四步、并发清除(和用户线程一起)

基于标记结果,直接清理对象

由上可看出这个收集器是尽可能减少全局停顿,特别步骤还是要全局停顿的。

工作原理:

这里之所以使用的并发清除,而不是并发压缩,有图可看,在清理的时候依然有程序在运行;所以我们就可以简单的把对象给移除清理掉。如果是并发整理或者并发压缩的情况下,他需要去重新整理这个内存空间,把要清理的对象移动位置的话,应用程序就很难在执行了;因此为了达到更好的或者是简单的实现,所以才用简单的清除算法,而非清除压缩等。

1、特点

(1)、尽可能降低停顿

(2)、会影响系统整体吞吐量和性能

比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半

(3)、清理不彻底

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

(4)、因为和用户线程一起运行,不能在空间快满时再清理(前面所说的收集器,则可以,因为他们都是停顿的,当清理之后又有大量空间)

-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值

如果不幸内存预留空间不够,就会引起concurrent mode failure

2、cms并发清理当中产生的错误原因

这是并发模式的错误,是因为在并发清理的时候她去试图清理垃圾对象,在这过程当中应用程序又在不断地进行内存申请,导致内存不够用了。因此遇到这个错误,可行的方案是:可以把所有应用程序暂停,改用串行收集器,来回收这些空间,事实上,cms也是这么错的,把串行收集器当做后备收集器。 

3、这里简单说明下标记清除与标记压缩区别

标记清除会有大量碎片产生

 标记-清除:就是标记对象之后,进行简单从内存移除。

 标记-压缩:就是标记压缩把可用的对象移到内存的一端,然后将边界之外的对象清理。

因此标记清除之后的内存空间可能是杂乱的,有大量碎片产生。标记压缩之后的内存空间是连续的, 没有碎片。所以标记清除对内存分配是有影响的,比如分配连续5个内存空间标记清除如图左,他是分配不出来的,虽然内存数量大于5.因此在标记清除之后还有一个必不可少的步骤就是压缩整理。这就为什么串行回收器与并行回收器都在用标记压缩算法的原因。没有碎片的产生因此效率还是不错的。为什么cms使用标记清除呢?因为他更加关注停顿。他希望能够在做gc的时候与应用线程一起运行,如果采用标记压缩算法,就要移动可用的对象空间,应用程序可能就很难找到需要用到的对象,为了能够与应用程序并发执行,就需要知道对象的位置,应用程序就能找到所需的对象。

因此cms在解决这个问题,在cms之后加了一步整理。

-XX:+ UseCMSCompactAtFullCollection Full GC后,在每一次Full gc之后,也就是cms之后进行一次碎片整理,这个整理过程是独占的。因此整理过程是独占的,会引起停顿时间变长,因为需要移动对象内存空间。

-XX:+CMSFullGCsBeforeCompaction

设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads

设定CMS的线程数量 一般等于cpu数量,不易设置太大。

因此cms并没有本质上的解决问题,只是减少停顿时间。

5、减少gc压力可以从哪方面考虑

6、收集器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回收

六、GC参数 – Tomcat实例

环境

Tomcat 7

JSP

网站

测试网站吞吐量和延时

工具

JMeter

目的

让Tomcat有一个不错的吞吐量

测试系统结构

Jmeter

性能测试工具

建立10个线程,每个线程请求Tomcat 1000次 共10000次请求

下面使用不同堆空间大小与不同收集器对比效果:

环境为JDK6:使用32M堆处理请求

设置参数:

set CATALINA_OPTS=-server -Xloggc:gc.log -XX:+PrintGCDetails -Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseSerialGC -XX:PermSize=32M 

(参数解析:在CATALINA_OPTS中添加如下参数:首先是-server 使用server模式,其次是设置loggc的路径,然后是想获取相信信息,所以使用PrintGCDetails参数,Xmx32M -Xms32M都是32兆所以表示系统启动时只能拿到32兆的堆空间。最大也只能拿到32兆。HeapDumpOnOutOfMemoryError 是希望拿到heapDump文件,用于分析,然后是使用古老的UseSerialGC串行回收器;XX:PermSize永久区设置为32兆。 )

运行结果如下图:吞吐量为540  最大延迟135,最小2秒

Samples表示1万次请求,Average平均是6毫秒,Median表示一般的请求都能在4毫秒响应,line表示百分之90的数据在7毫秒响应,Throughput吞吐量为540  MAX最大延迟135,min最小2秒,Error表示错误产生,这里则是没有错误产生,tomcat没有产生拒绝连接的错误。

上图是在系统大量请求占据大量内存后的fullGC ,有图可看从32到33秒之间产生4次的gc,而且时间很短;每次fullgc都会产生0.08秒的时间。所以为了获得更好的性能,所以最直接的做法是增大堆空间,如下:

JDK6:使用最大堆512M堆处理请求(提高性能测试,增大堆空间)

参数:

set CATALINA_OPTS=-Xmx512m -XX:MaxPermSize=32M  -Xloggc:gc.log -XX:+PrintGCDetails

结果:FULL GC很少,基本上是Minor GC

运行时只有16兆;下面可看增加60兆, gc后堆空间 由16增加到60兆

JDK6:使用最大堆512M堆处理请求

参数:

set CATALINA_OPTS=-Xmx512m -Xms64m -XX:MaxPermSize=32M  -Xloggc:gc.log -XX:+PrintGCDetails

结果 GC数量减少 大部分是Minor GC

JDK6:使用最大堆512M堆处理请求

参数:

set CATALINA_OPTS=-Xmx512m -Xms64m -XX:MaxPermSize=32M  -Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=4

结果:GC压力原本不大,修改GC方式影响很小

JDK 6

set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M  -Xloggc:gc.log -XX:+PrintGCDetails

减小堆大小,增加GC压力,使用Serial回收器

JDK 6

set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M  -Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParallelOldGC -XX:ParallelGCThreads=4

减小堆大小,增加GC压力,使用并行回收器

JDK 6

set CATALINA_OPTS=-Xmx40m -Xms40m -XX:MaxPermSize=32M  -Xloggc:gc.log -XX:+PrintGCDetails -XX:+UseParNewGC

减小堆大小,增加GC压力,使用ParNew回收器

启动Tomcat 7

使用JDK6

不加任何参数启动测试(是为了跟JDK7对比较)

启动Tomcat 7

使用JDK7

不加任何参数启动测试

由此可看吞吐量由622增加到680毫秒

因此升级JDK可能会带来额外的性能提升!不要忽视JDK的版本哦

七、总结

性能根本在于应用,所以提高性能主要在应用,gc参数只是在细节性微调。如果设计不好,代码质量等等在应用层没有处理好,只靠gc参数不能很好地解决性能问题,甚至更低。调节gc参数赫和回收器本质上是解决不了根本问题的。如果比如gc参数设置的不合理,还会使系统产生大的延迟。所以gc参数属于应用程序的细节处理级别。

猜你喜欢

转载自blog.csdn.net/gududedabai/article/details/81176390
今日推荐