JVM性能优化之老年代参数设置
一、引言
咱们书接上回,上篇文章主要讲解了年轻代参数设置,如果对这一部分还不清楚的建议先去看一下(年轻代参数设置),本文主要为大家介绍老年代参数的设置,掌握好jvm参数的设置是一个高级开发人人员必备的技能,也是面试中最爱被问的一部分,接下来我们就来看看如何设置老年代参数。
二、对象进入老年代的时机
在JVM中,对象从年轻代晋升到老年代的条件主要包括以下几种情况:
(1)年龄阈值
- 对象在年轻代的Survivor区(From和To)之间每次Minor GC后存活,年龄(Age)会+1。
- 当对象的年龄达到阈值(默认值:-XX
:
MaxTenuringThreshold=15,CMS下默认为6),下次GC时会晋升到老年代。
注:CMS 通常与 ParNew 收集器搭配使用(年轻代采用 ParNew,老年代采用 CMS)。
ParNew 的 MaxTenuringThreshold默认值为 6(CMS 场景下),而 Parallel Scavenge 的默认值是 15。
(2)大对象直接进入老年代
- 大对象(如长数组或大字符串)会直接分配到老年代,避免在年轻代频繁复制。
- 通过参数
-XX:PretenureSizeThreshold
设置大对象的阈值(默认值为0,表示未启用。仅对Serial和ParNew收集器有效)。
(3)Survivor区空间不足
- 如果Survivor区中相同年龄的所有对象大小总和超过Survivor区的一半(
TargetSurvivorRatio
默认50%),年龄≥该年龄的对象会直接晋升到老年代,无需达到MaxTenuringThreshold。
(4)Minor GC后存活对象过多
- Minor GC后,存活的对象无法全部放入Survivor区,则会通过分配担保机制(Handle Promotion)直接进入老年代。
(5)动态年龄判定(HotSpot优化策略)
- HotSpot虚拟机并非严格按照MaxTenuringThreshold晋升,而是会动态计算,如果Survivor区中某年龄的所有对象大小总和 > Survivor区的50%(
TargetSurvivorRatio
),则年龄≥该年龄的对象会直接晋升。
(6)老年代分配担保(空间分配担保)
- 在Minor GC前,JVM会检查老年代剩余空间是否大于年轻代所有对象总大小(或历次晋升的平均大小)。
- 如果不足,则触发Full GC;若Full GC后仍不足,则部分对象会直接进入老年代(取决于垃圾收集器策略)。
三、老年代垃圾回收参数设置
看到这里大家应该都知道了对象在什么时机下会进入老年代,接着我们再来看看在年轻代参数优化里面说到的那个案例:
每日上亿请求量的电商系统,那么大家可以来推算一下每日上亿请求量的电商系统,他会每日有多少活跃用户?一般按每个用户平均访问20次来计算,那么上亿请求量,大致需要有500万日活用户。那么继续来推算一下,这500万的日活用户都是会进来进行大量的浏览,那么多少人会下订单?这里可以按照10%的付费转化率来计算,每天大概有50万人会下订单,那么大致就是每天会有50万订单。这50万订单算他集中在每天4小时的高峰期内,那么其实平均下来每秒钟大概也就几十个订单,大家是不是觉得根本没啥可说的?
但是如果考虑到大促场景类似于双十一节日,可能在大促开始的短短10分钟内,瞬间就会有50万订单,那么此时每秒就会有接近1000的下单请求,我们就针对这种大促场景来对订单系统的内存使用模型分析一下。
机器数量按3台来算,就是每台机器每秒需要抗300个下单请求。这个也是非常合理的,而且需要假设订单系统部署的就是最普通的标配4核8G机器
从机器本身的CPU资源和内存资源角度,抗住每秒300个下单请求是没问题的。
经过我们对他内存模型的估算之后,进行了年轻代的参数优化,看看优化之后的语句:
“-Xms3072M -Xmx3072M-Xmn2048M -Xss1M-XX:PermSize=256M -XXMaxPemmSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC.XX:+UseConcMarkSweepGC"
“-XX:MaxTenuringThreshold=5”这个参数会让在一段时间内连续躲过5次MinorGC的对象迅速进入老年代中。
这种对象一般就是一些@Service、@Controller之类的注解标注的那种系统业务逻辑组件,这种对象实例一般全局就有一个实例就可以了,要一直使用的。所以一般会长期被GC Roots引用,这种对象一般不会太多,大概最多一个系统就几十MB这种对象。
所以此时类似这样的长期存活的对象就会进入老年代中,如下图所示。
此外就是Minor GC过后可能存活的对象超过200MB放不下Sunivor了,或者是一下子占到超过Surviovr的50%,此时会有一些对象进入老年代中。
但是我们之前对新生代的JM参数进行优化,就是为了避免这种情况,经过我们的测算,这种概率应该是很低的。但是虽说是很低,也不能完全是是没有这种情况,比如某一次GC过后可能刚好机缘巧合有超过200MB对象,就会进入老年代里。
我们可以做一个假设,大概就是这个系统在业务高峰期间,每隔5分钟会在Minor GC之后有一小批对象进入老年代,大概200MB左右的大小,如下图所示。
3.1 高峰期间哎多久出发Full GC
根据我们目前优化的结果来看,Full GC的触发条存在以下三个:
(1)每次Minor GC之前,都检査一下“老年代可用内存空间”<“历次Minor GC后升入老年代的平均对象大小“
其实按照我们目前设定的背景,要很多次Minor GC之后才可能有一两次碰巧会有200MB对象升入老年代,所以这个“历次Minor GC后升入老年代的平均对象大小”,基本是很小的。
(2)可能某次Minor GC后要升入老年代的对象有几百MB,但是老年代可用空间不足了
(3)设置了"-XX:CMSInitiatingOccupancyFaction"参数,比如设定值为92%,那么此时可能前面几个条件都没满足,但是刚好发现这个条件满足了,比如就是老年代空间使用超过92%了,此时就会自行触发Ful GC
其实在真正的系统运行期间,可能会慢慢的有对象进入老年代,但是因为新生代我们优化过了内存分配,所以对象进入老年代的速度是很慢的。所以很可能是在系统运行半小时~1小时之后,才会有接近 1GB的对象进入老年代。
此时可能会因为上述的条件中任何一个满足了,就触发FuII GC。但是这三个条件一般都需要老年代近乎占满的时候,才有可能会触发。
大家可以思考一下,我们假设在业务高峰期间,系统运行1小时之后,高峰期几乎都快过了,此时才可能会触发一次Full GC(结合自己的系统去考虑,如果自己的系统业务高峰期持续时间较长,结合自己系统去优化参数,优化的思路的都是一致的),业务高峰期过后,系统压力减小,GC的问题就不算什么了。
此时JVM参数如下:
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPemSize=256M .XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGCXX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92”
3.2 垃圾碎片的整理
在CMS完成Full GC之后,一般需要执行内存碎片的整理,可以设置多少次Full GC之后执行一次内存碎片整理,但是我们有必要修改这些参数吗?
其实没必要,因为通过前面的分析,在高峰期,Full GC可能也就1小时执行一次,然后高峰期过去之后,就没那么多的请求了,此时可能几个小时才会有一次Full GC。
所以就保持默认的设置,每次Full GC之后都执行一次内存碎片整理就可以,目前JVM参数如下:
“Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPemmSize=256MXX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC.XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92.XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0"
四、小结
如果把JVM性能优化的两篇文章都看完,就能发现,Full GC优化的前提是Minor GC的优化,Minor GC的优化的前提是合理分配内存空间合理分配内存空间的前提是对系统运行期间的内存使用模型进行预估。我们只要对系统运行期间的内存使用模型做好预估,然后分配好合理的内存空间,尽量让Minor GC之后的存活对象留在Sunvivor里不要去老年代,然后其余的GC参数不做太多优化,系统性能基本上就不会太差。