JVM性能调优篇04-垃圾收集算法与垃圾收集器ParNew&CMS详解

垃圾收集算法

在这里插入图片描述

分代收集理论

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块,一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如新生代中,每次收集都会有大量的对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择标记-清除或者标记-整理算法进行垃圾收集。

标记-清除标记-整理算法会比复制算法慢10倍以上。

标记复制算法

复制算法将内存分为两个区间,这两个区间是动态的,在任意一个时间点,所有分配的对象内存只能在其中一个区间(活动区间),另外一个区间就是空闲区间。
当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址一次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。这个时候空闲内存已经变成了活动区间,垃圾对象全部在原来的活动区间,清理掉垃圾对象,原活动区间就变成了空闲区间。

这种方式内存的代价太高,每次基本上都要浪费一半的内存。于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存是Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)。

在这里插入图片描述

标记-清除算法

顾名思义,标记-清除算法分为两个阶段,标记(mark)和清除(sweep)。
标记:标记已存活的对象(GC Roots+GC Roots所引用的对象),这些对象标记为存活状态
清除:清除的过程将遍历所有堆中的对象,将没有标记的对象全部清除。

也可以反过来标记出所有可回收的对象,在标记完成之后统一回收所被标记的对象。

标记-清除算法会带来两个明显的问题:

  1. 效率问题(如果需要标记的对象太多,效率不高):执行效率不稳定,如果Java堆中包含大量对象,而且大部分是需要被回收的,这时必须记性大量标记及清除动作,导致标记和清除两个过程执行效率都随对象数量增长而降低。
  2. 空间问题(标记清除后会产生大量不连续的碎片):内存空间碎片化的问题,标记、清除后会产生大量的不连续内存碎片,空间碎片太可能会导致当以后需要分配大对象时无法找到足够的连续内存二不得不提前触发另一次垃圾收集动作。
    在这里插入图片描述

标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述
在这里插入图片描述

我们分代收集理论基于不同的代,比如年轻代、老年代根据他们的不同的特点选择对应的垃圾算法。
比如我们年轻代都是一些朝生即死的对象,所以我们年轻代选择复制算法是可以的,老年代一般会用标记整理算法或标记清除算法。

垃圾收集器

在这里插入图片描述

如果说收集算法是内存回收的方法论,那么垃圾收集器就是具内存回收的具体实现

虽然我们对各个垃圾收集器进行比较,但并非为了挑选一个最好的垃圾处理器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,**我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。**试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的Java虚拟机就不会实现那么多不同的垃圾收集器了。

除了图上的垃圾收集器以外还有一些其他垃圾收集器,基于市面上常用的jdk版本,了解上边的垃圾收集器即可。

Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

-XX:+UseSerialGC : 年轻代使用Serial收集器
-XX:+UseSerialOldGC:老年代使用SerialOld收集器
Serial收集器新生代采用复制算法,老年代采用标记-整理算法。
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。

这个垃圾收集器意味着它只会使用一条垃圾收集线程去完成垃圾收集工作、并且进行垃圾收集工作时,要暂停其他所有的工作线程(Stop The World), 直到它收集结束。

在这里插入图片描述

但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案

Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

Parallel Scavenge收集器新生代采用复制算法,老年代采用标记-整理算法。
Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改,没必要修改。

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

在这里插入图片描述

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

在单核的服务器中用Serial收集器的效率往往高于Parallel收集器。

ParNew收集器(-XX:+UseParNewGC)

ParNew收集器新生代采用复制算法,老年代采用标记-整理算法。
ParNew收集器其实跟Parallel收集器很类似,区别在于它可以和CMS收集器配合使用

在这里插入图片描述
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器配合工作。

CMS收集器(-XX:+UseConcMarkSweepGC(old)) 重点

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作

CMS收集器是一种标记-清除算法实现的,它的运作过程比较复杂一些。整个过程分为4个步骤:

  1. 初始标记:暂停所有的其他线程(STW), 并标记下被GC Roots直接引用的对象,速度很快很快,由于只标记GC Roots直接引用的对象,所以STW时间会特别特别短。
  2. 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。这个阶段已经把可以标记的基本都标记了,但是会有一些对象的标记状态会有一些问题。这些有问题的对象还会经过重新标记,就是下一道步骤。
  3. 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。重新标记也会STW
  4. 并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
  5. 并发重置:重置本次GC过程中的标记数据。在这个过程中对很多对象打了标记,既然这次垃圾收集任务已经结束,对之前标记的对象清除标记。

在这里插入图片描述

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:

  1. 对CPU资源敏感(会和服务抢资源)
  2. 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
  3. 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
  4. 执行过程中的不确定性,比如当你执行并发标记或者并发清理时用户的线程一直在允许,这时候有可能会有新的垃圾进入老年代,这时候老年代已经是放不下新的对象了,这时候会触发"concurrent mode failure(并发失败)",此时会进入stop the world,用serial old垃圾收集器来回收

CMS的相关核心参数:

  1. -XX:+UseConcMarkSweepGC:启用cms垃圾收集器,jdk8推荐CMS,jdk9推荐G1
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩整理,默认是0,代表每次FullGC后都会压缩一次
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比),如果老年代满了之后触发full gc,由于cms的并发收集特性,设置该参数,避免发生concurrent mode failure
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段。老年代的对象也有可能会引用年轻代的对象,如果GMS GC之前启动一次minor gc,有可能会直接干掉这些对象,然后回去再做Full GC收集老年代的对象时,并发标记的过程会比较快,这个参数可以设置也可以不用设置,因为老年代直接引用年轻代的对象不会很多,老年代很少有跨代引用对象的场景。
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

亿级流量电商系统如何优化JVM参数设置(ParNew+CMS)

大型电商系统后端现在一般都是拆分为多个子系统部署的,比如,商品系统,库存系统,订单系统,促销系统,会员系统等等。
我们这里以比较核心的订单系统为例
在这里插入图片描述
以上图的例子讲解JVM参数调优案例
促销场景下假如每秒产生1000单的订单,我们的订单服务部署在了3台4核8G的服务器之中,那么没台服务器没秒创建300单,假设订单对象大小为1KB,,那么每秒会有300KB的对象产生,由于下单系统还会涉及用户积分,优惠券,库存等,我们放大20倍,并且订单服务不单单只有下单的功能,还有订单查询等其他操作,我们再放大10倍,那么计算结果每秒会产生60MB对象。

对于8G内存的服务器我们一般是分配4G内存给JVM,正常的JVM参数配置如下:

-Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8

相当于整个堆设置了3G,老年代2G,年轻代1G,Eden区和Surivior区的比例设置为8:1:1,因此Eden区800M,S0,S1区域都为100M,方法区设置256MB,线程栈设置1M

由于每秒创建60M对象,不考虑其他的系统对象前提下,14秒会塞满Eden区,然后下一秒的60MB新对象再进来时由于Eden区放满之后会发生minor gc。这时之前14秒中创建的对象由于线程结束,已经变成了可以回收的对象,但是新进来的60MB对象有可能还不能回收,准备进入Survivor区,由于上篇博客中讲解的动态对象年龄判断原则,当要进入Surivivor区的对象和Surivivor区的对象之后大于50%的部分会直接进入老年代,那么导致会经常发生full gc,很影响程序性能。

于是我们可以更新下JVM参数设置:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8

堆设置3G
老年代1G
年轻代设置2G
Eden区1.6G
S0,S1 各200M
方法区256M

这样就降低了因为对象动态年龄判断原则导致的对象频繁进入老年代的问题,其实很多优化无非就是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc。
在这里插入图片描述

对于对象年龄应该为多少才移动到老年代比较合适,本例中一次minor gc要间隔二三十秒,大多数对象一般在几秒内就会变为垃圾,完全可以将默认的15岁改小一点,比如改为5,那么意味着对象要经过5次minor gc才会进入老年代,整个时间也有一两分钟了,如果对象这么长时间都没被回收,完全可以认为这些对象是会存活的比较长的对象,可以移动到老年代,而不是继续一直占用survivor区空间。
对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold),这个一般可以结合你自己系统看下有没有什么大对象生成,预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过1M的大对象,这些对象一般就是你系统初始化分配的缓存对象,比如大的缓存List,Map之类的对象。
可以适当调整JVM参数如下:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8 
-XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M 

对于JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),如果内存较大(超过4个G,只是经验值),系统对停顿时间比较敏感,我们可以使用ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC),这个组合也是大部分互联网公司在JDK1.8环境下使用的组合

对于老年代CMS的参数如何设置我们可以思考下,首先我们想下当前这个系统有哪些对象可能会长期存活躲过5次以上minor gc最终进入老年代。
无非就是那些Spring容器里的Bean,线程池对象,一些初始化缓存数据对象等,这些加起来充其量也就几十MB。
还有就是某次minor gc完了之后还有超过一两百M的对象存活,那么就会直接进入老年代,比如突然某一秒瞬间要处理五六百单,那么每秒生成的对象可能有一百多M,再加上整个系统可能压力剧增,一个订单要好几秒才能处理完,下一秒可能又有很多订单过来。
我们可以估算下大概每隔五六分钟出现一次这样的情况,那么大概半小时到一小时之间就可能因为老年代满了触发一次Full GC,Full GC的触发条件还有我们之前说过的老年代空间分配担保机制,历次的minor gc挪动到老年代的对象大小肯定是非常小的,所以几乎不会在minor gc触发之前由于老年代空间分配担保失败而产生full gc,其实在半小时后发生full gc,这时候已经过了抢购的最高峰期,后续可能几小时才做一次FullGC。
对于碎片整理,因为都是1小时或几小时才做一次FullGC,是可以每做完一次就开始碎片整理,或者两到三次之后再做一次也行。
综上,只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值,如下所示:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8 
-XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 
-XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3

上边JVM参数意思为
堆 3G
老年代 1G
年轻代 2G
Eden区 1.6G
S0,S1各200M
线程栈 1M
方法区256M
分代年龄 5
大对象直接进入老年代 1M
年轻代垃圾收集器 ParNew
老年代垃圾收集器 CMS
老年代达到百分之多少空间执行full gc 92%
UseCMSCompactAtFullCollection full gc之后整理内存碎片
多少次full gc之后整理内存碎片 3次

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值