java 九 (一) 实战
案例实战:每秒十万QPS的社交APP 如何优化GC性能提升3倍
案例背景
本案例的背景是一个有高峰期每秒十万QPS的社交APP,QPS 的英文全称就是 “Query Per Second”,也就是每秒钟的查询数量,大致可以理解为是APP每秒钟的访问数量。
可以用下面一幅图来让大家了解一下这个社交APP的一个情况

高并发查询导致对象快速进入老年代
这里就直接给大家分析一下当时案例发生的一个场景,因为这个社交APP的日活用户涨的很快,所以导致他的高峰期QPS很快就飙升到了10万。
正是因为每秒并发量太高,这也直接导致了这个系统在高峰期的时候,年轻代的Eden区会迅速的被填满,并且频繁的触发Young GC,如下图所示。

而且每次在Young GC的时候,实际上还有很多请求是没处理完毕的,没办法,因为每秒请求量太多了,所以在你触发Young GC的时候,就这一瞬间,必然有很多请求是还没处理完毕的。
这就导致Eden区中其实每次触发Young GC的时候,都有很多对象是需要存活下来的,如下图所示:

因此在高峰期的时候,其实经常会出现Young GC过后存活对象较多,在Survivor区中放不下的问题,如下图所示。

所以此时必然会导致大量的对象快速的进入老年代中,如下图所示。

老年代必然会触发频繁GC
其实根据之前学过的知识,大家都清楚了一点,那就是一旦在高并发场景下Young GC后存活对象过多,导致对象快速进入老年代,必然会频繁触发老年代的GC,对老年代进行垃圾回收。
所以在上述社交APP高峰期高并发场景下,必然会导致个人主页服务对应的JVM频繁的发生老年代的GC,如下图所示。

优化前的线上系统JVM参数
其实大家都知道,针对上述场景,最核心的优化点,主要应该是增加机器,尽量让每台机器承载更少的并发请求,减轻压力。
同时,给年轻代的Survivor区域更大的内存空间,让每次Young GC后的存活对象务必停留在Survior中,别进入老年代。
但是在这里我们先不考虑上述优化,在优化前的线上系统中,对JVM有两个比较关键的参数大家可以看一下:
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5
大家可以看到这个是什么意思,其实非常明显,我们之前讲过这两个参数的含义。CMS垃圾回收器默认是采用标记-清理算法,所以是会造成大量的内存碎片的。
什么叫内存碎片?我们再来看下图回顾一下
比如现在老年代内存里有一些垃圾对象。

然后CMS垃圾回收器一次垃圾回收过后,回收掉了一些垃圾对象,此时可能内存里看起来跟下面这样:

大家注意上面那个红圈的地方,因为回收掉了一个对象,所以那里出现了一个内存碎片
虽然这里是空白内存,但是假如此时你要是要分配一个对象比较大,没法再上面红圈处放进去呢?那么红圈的那个内存碎片不就没任何意义了?
所以CMS正常垃圾回收,因为使用标记-清理算法,所以必然导致大量的内存碎片。
所以“-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5”两个参数的含义,就是在5次Full GC之后会触发一次Compaction操作,也就是压缩操作
这个操作会把存活对象放到紧邻在一起,避免大量的内存碎片,如下图所示。

大家看下上图,是不是发现两个存活对象被挤压在一起了?然后红圈地方是不是多出来一大块连续可用的内存空间?不再是之前的一小片内存碎片了吧?
频繁Full GC导致的大量内存碎片
但是大家现在要明白一点,上述两个参数“-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5”是设置的5次Full GC之后才会进行一次压缩操作,解决内存碎片的问题,空出来大片的连续可用内存空间。
所以这就直接导致在这5次Full GC的过程中,每一次Full GC之后都会产生大量的内存碎片。
大量的内存碎片会导致很多问题,其中一个问题,就是提高Full GC的频率。
为什么呢?因为大家之前应该还记得我们讲过,触发老年代GC的一个非常重要的条件,就是Young GC后的存活对象无法放入Survivior,就要放入老年代。
但是此时老年代假设也没足够内存放这些对象了,就必须触发Full GC了。
所以大家考虑一个场景,假设如下图所示,一次Full GC过后,老年代中有一部分内存里都是大量的内存碎片,没法放入完整的一些大对象了,只有部分内存是连续可用的内存空间。
这个时候,随着大量对象快速进入老年代,会导致一旦老年代的那块连续可用内存满了,此时很多内存碎片是无法放入更多对象的,就会立马触发下一次Full GC。
比如老年代有2G的内存,其中1.5G是连续可用内存,0.5G是很多内存碎片。
本来老年代如果都是连续空内存的话,那么可能可以对象占用到将近2G才会触发Full GC。
结果现在就是对象占用到了1.5G就需要触发Full GC了,剩下0.5G是没法放任何对象的。
所以这就会导致随着一次一次Full GC导致老年代产生更多的内存碎片,连续可用内存越来越少,触发下一次FUll GC的速度就会越快。
直到几次Full GC之后,才会触发一次Compaction操作去整理内存碎片。
这个案例如何进行优化?
其实对这个案例进行优化,非常的简单,无法就是用之前讲过的jstat分析一下各个机器上的jvm的运行状况,判断出来每次Young GC后存活对象有多少,然后就是增加Survivor区的内存,避免对象快速进入老年代。
另外一个,在当时对那个系统优化之后,增加了年轻代和Survivor区的大小,但还是会慢慢的有对象进入老年代里,毕竟系统负载很高,彻底让对象不进入老年代也很难做到。所以当时调优过后每小时还是会有一次Full GC。
所以当时第二个参数的优化就是针对CMS内存碎片问题的
在降低了Full GC频率之后,务必设置如下参数“-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0”,每次Full GC后都整理一下内存碎片。
否则如果每次Full GC过后,都造成老年代里很多内存碎片,那么必然导致下一次Full GC更快到来,因为内存碎片会导致老年代可用内存变少。
也许第一次Full GC是一小时才有,第二次Full GC也许是40分钟之后,第三次Full GC可能就是20分钟之后,要是不解决CMS内存碎片问题,必然导致Full GC慢慢变得越来越频繁。
案例实战:垂直电商APP后台系统,如何对Full GC进行深度优化?
垂直电商APP的JVM性能问题
很简单,问题就出在类似这样的一个创业型互联网公司,虽然有少数几个技术比较好的架构师,但是架构师往往没那么大精力把控到特别细节的地方所以大部分的一线普通工程师可能都对JVM这块没有那么的精通,起码说对我们的专栏里讲解的JVM相关原理和优化手段,都没了解。所以这直接导致一个很大的问题,那就是大部分的一线工程师开发完一个系统之后,部署生产环境的时候往往就不会对JVM进行什么参数的设置,可能很多时候就是用一些默认的JVM参数。默认的JVM参数绝对是系统负载逐渐增高的时候一个最大的问题如果你不设置-Xmx、-Xms之类的堆内存大小的话,你启动一个系统,可能默认就给你几百MB的堆内存大小,新生代和老年代可能都是几百MB的样子。所以当时这个垂直电商APP的很多后台系统,基本都是用的默认JVM参数部署启动的,前期是没什么问题,但是中后期开始,当有一定用户量,有一定负载了,此时就会出现一些问题了。大家通过之前大量的学习,哪怕现在不画图,脑子里都有一个概念了,那就是新生代内存过小,会导致Survivor区域内存过小,同时Eden区域也很小。
Eden区域过小,自然会导致频繁的触发Young GC,Survivor区域过小,自然会导致经常在Young GC之后存活对象其实也没多少,但就是Survivor区域放不下。
此时必然会导致对象经常进入老年代中,因此也必然会导致老年代过一段时间就放满了,然后就会触发Full GC。
所以当时这个垂直电商APP的各个系统通过jstat分析JVM GC之后发现,基本上高峰期的时候,Full GC每小时都会发生好几次。
Full GC一般在正常情况下,都是以天为单位发生的,比如每天发生一次,或者是几天发生一次Full GC。
要是每小时都发生几次Full GC,那么就会导致系统每小时都卡顿好几次。这个时候必然是不行的。
在这个背景下,当时我的朋友是那家公司的架构师,找到我帮忙,在分析系统情况过后,定制了一套公司级别的JVM参数模板
在大部分工程师都对JVM优化不是很精通的情况下,通过推行一个JVM参数模板,让各个系统短时间内迅速就优化了JVM的性能。
公司级别的JVM参数模板
其实这个公司级别的或者团队级别的JVM参数模板,是一个很有用的东西,因为大家要知道,并不是每个人都会来学习我们的这个JVM专栏,也就意味着并不是每个人都是可以比较精通JVM的核心运行原理和性能优化的。
所以如果你是一个团队的leader,或者是一个中小型公司的架构师,那么必然是需要为团队或者公司定制一套基本的JVM参数模板的
然后尽量让大部分系统套用这个模板,基本保证JVM性能别太差,避免很多初中级工程师直接使用默认的JVM参数,可能一台8G内存的机器上,JVM堆内存就分配了几百MB。
下面我们可以来看看当时我和那位朋友一起定制出来的适合他们公司的JVM参数模板:
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
为什么如此定制JVM参数模板呢?
首先,8G的机器上给JVM堆内存分配4G就差不多了,毕竟可能还有其他进程会使用内存,一般别让JVM堆内存把机器内存给占满。
然后年轻代给到3G,之所以给到3G的内存空间,就是因为让年轻代尽量大一些,进而让每个Survivor区域都达到300MB左右。
根据当时对这个业务系统的分析,假设用默认的JVM参数,可能年轻代就几百MB的内存,Survivor区域就几十MB的内存
那么每次垃圾回收过后存活对象可能会有几十MB,这是因为在垃圾回收的一瞬间可能有部分请求没处理完毕,此时会有几十MB对象是存活的,所以很容易触发动态年龄判定规则,让部分对象进入老年代。
所以在分析过后,给年轻代更大内存空间,让Survivor空间更大,这样在Young GC的时候,这一瞬间可能有部分请求没处理完毕,有几十MB的存活对象,这个时候在几百MB的Survivor空间中可以轻松放下,绝对不会进老年代。
基本上在这个内存分配之下,对于这个垂直电商APP的大部分后台业务系统,都是可以轻松hold住的
不同的系统运行时的情况略有不同,但是基本上都是在每次Young GC过后存活几MB~几十MB的对象,所以此时在这个参数模板下,都可以抗住。
只要把内存分配完毕,那么对象进入老年代的速度是极慢极慢的,经过这个参数模板在朋友公司全部系统的重新部署和上线,各个团队通过jstat观察,基本上发现各个系统的Full GC都变成了几天才会发生一次。
此时在参数模板里还会加入Compaction相关的参数,保证每次Full GC之后都会执行一次压缩,解决内存碎片的问题。
关于内存碎片的影响和优化,上一篇文章刚刚分析过,那是为另外一个朋友的公司专门做优化的时候调整的参数。
如何优化每次Full GC的性能?
这里给大家再介绍一下当时帮那位朋友做优化的时候调整的另外两个参数,这个两个参数可以帮助优化FUll GC的性能,把每次Full GC的时间进一步降低一些。
一个参数是"-XX:+CMSParallelInitialMarkEnabled",这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行。
大家应该还记得初始标记阶段,是会进行 Stop the World 的,会导致系统停顿,所以这个阶段开启多线程并发之后,可以尽可能优化这个阶段的性能,减少 Stop the World 的时间。
另外一个参数是 "-XX:+CMSScavengeBeforeRemark",这个参数会在CMS的重新标记阶段之前,先尽量执行一次 Young GC。
其实大家都记得,CMS的重新标记也是会 Stop the World 的,所以所以如果在重新标记之前,先执行一次 Young GC,就会回收掉一些年轻代里没有人引用的对象。
所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。
所以当时在JVM参数模板中,同样加入了这两个参数:
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark
垂直电商APP全公司采用JVM参数模板之后的效果
上述JVM参数模板,推广到了朋友公司的全部系统中,因为当时公司里几乎一线工程师对JVM优化的理解都达不到这个专栏讲解的水准,很多人都理解非常粗浅的,所以这套JVM模板参数是全部推行的。
经过各个团队采用jstat观察JVM GC情况,发现明显有了很大的好转,基本上各个系统的Young GC都在几分钟一次,或者十几分钟一次,每次耗时就几十毫秒而已。
Full GC基本都在几天一次,每次耗时在几百毫秒的样子。
基本上各个系统的JVM达到这个性能,就对线上系统没多大影响了。哪怕是不太懂JVM优化的普通工程师只要套用这个模板,对一些普通的业务系统,都能保证其JVM性能不会出现大的问题,比如频繁的Young GC和Full GC导致的系统频繁卡顿。

浙公网安备 33010602011771号