jvm性能调优 - 16案例实战_每日上亿请求量的电商系统 年轻代垃圾回收参数如何优化

简介: jvm性能调优 - 16案例实战_每日上亿请求量的电商系统 年轻代垃圾回收参数如何优化

案例背景

按照惯例,我们接下来会用案例驱动来带着大家分析到底该如何在特定场景下,预估系统的内存使用模型

然后合理优化新生代、老年代、Eden和Survivor各个区域的内存大小,接着再尽量优化参数避免新生代的对象进入老年代,尽量让对象留在新生代里被回收掉。

我们这里的背景是电商系统,电商系统其实一般会拆分为很多的子系统独立部署,比如商品系统、订单系统、促销系统、库存系统、仓储系统、会员系统,等等

我们这里就以比较核心的订单系统作为例子来说明。

我们的案例背景是每日上亿请求量的电商系统,那么大家可以来推算一下每日上亿请求量的电商系统,他会每日有多少活跃用户?

一般按每个用户平均访问20次来计算,那么上亿请求量,大致需要有500万日活用户。

那么继续来推算一下,这500万的日活用户都是会进来进行大量的浏览,那么多少人会下订单?

这里可以按照10%的付费转化率来计算,每天大概有50万人会下订单,那么大致就是每天会有50万订单。

这50万订单算他集中在每天4小时的高峰期内,那么其实平均下来每秒钟大概也就几十个订单,大家是不是觉得根本没啥可说的?

因为几十个订单的压力下,根本就不需要对JVM多关注,基本上就是每秒钟占用一些新生代内存,隔很久新生代才会满,然后一次Minor GC后垃圾对象清理掉,内存就空出来了,几乎无压力。


特殊的电商大促场景

但是如果你要是考虑到特殊的电商大促场景,就不会这么想了

因为很多中小型的电商平台,确实平时系统压力其实没那么大,也没太大的高并发,每秒几千并发压力就算是高峰压力了。

但是如果遇到一些大促场景,比如双11什么的,情况就不同了。

假设在类似双11的节日里,零点的时候,很多人等着大促开始就要剁手购物,这个时候,可能在大促开始的短短10分钟内,瞬间就会有50万订单。

那么此时每秒就会有接近1000的下单请求,我们就针对这种大促场景来对订单系统的内存使用模型分析一下。


抗住大促的瞬时压力需要几台机器?

那么要抗住大促期间的瞬时下单压力,订单系统需要部署几台机器呢?

基本上可以按3台来算,就是每台机器每秒需要抗300个下单请求。这个也是非常合理的,而且需要假设订单系统部署的就是最普通的标配4核8G机器。

从机器本身的CPU资源和内存资源角度,抗住每秒300个下单请求是没问题的。

但是问题就在于需要对JVM有限的内存资源进行合理的分配和优化,包括对垃圾回收进行合理的优化,让JVM的GC次数尽可能最少,而且尽量避免Full GC,这样可以尽可能减少JVM的GC对高峰期的系统新更难的影响。


大促高峰期订单系统的内存使用模型估算

背景已经全部说完了,接下来咱们就得来预估订单系统的内存使用模型了.

基本上可以按照每秒钟处理300个下单请求来估算,其实无论是订单处理性能还是并发情况,都跟生产很接近

因为处理下单请求是比较耗时的,涉及很多接口的调用,基本上每秒处理100~300个下单请求是差不多的。

那么每个订单咱们就按1kb的大小来估算,单单是300个订单就会有300kb的内存开销

然后算上订单对象连带的订单条目对象、库存、促销、优惠券等等一系列的其他业务对象,一般需要对单个对象开销放大10倍~20倍。

此外,除了下单之外,这个订单系统还会有很多订单相关的其他操作,比如订单查询之类的,所以连带算起来,可以往大了估算,再扩大10倍的量。

那么每秒钟会有大概300kb * 20 * 10 = 60mb的内存开销。但是一秒过后,可以认为这60mb的对象就是垃圾了,因为300个订单处理完了,所有相关对象都失去了引用,可以回收的状态。

大家看下图


内存到底该如何分配?

假设我们有4核8G的机器,那么给JVM的内存一般会到4G,剩下几个G会留点空余给操作系统之类的来使用,不要想着把机器内存一下子都耗尽,其中堆内存我们可以给3G,新生代我们可以给到1.5G,老年代也是1.5G。

然后每个线程的Java虚拟机栈有1M,那么JVM里如果有几百个线程大概会有几百M

然后再给永久代256M内存,基本上这4G内存就差不多了。

同时还要记得设置一些必要的参数,比如说打开“-XX:HandlePromotionFailure”选项,JVM参数如下所示

“-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M  -XX:HandlePromotionFailure

但是“-XX:HandlePromotionFailure”参数在JDK 1.6以后就被废弃了,所以现在一般都不会在生产环境里设置这个参数了。在JDK 1.6以后,只要判断“老年代可用空间”> “新生代对象总和”,或者“老年代可用空间”> “历次Minor GC升入老年代对象的平均大小”,两个条件满足一个,就可以直接进行Minor GC,不需要提前触发Full GC了。

所以实际上,如果大家用的是JDK 1.7或者JDK 1.8,那么JVM参数就保持如下即可,后面也都不再加入这个参数了:

“-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M”

此时JVM内存入下图所示。

接着就很明确了,订单系统的系统程序在大促期间不停的运行,每秒处理300个订单,都会占据新生代60MB的内存空间

但是1秒过后这60MB对象都会变成垃圾,那么新生代1.5G的内存空间大概需要25秒就会占满,如下图。

25秒过后就会要进行Minor GC了,此时因为有“-XX:HandlePromotionFailure”选项,所以你可以认为需要进行的检查,主要就是比较 “老年代可用空间大小”和“历次Minor GC后进入老年代对象的平均大小”,刚开始肯定这个检查是可以通过的。

所以Minor GC直接运行,一下子可以回收掉99%的新生代对象,因为除了最近一秒的订单请求还在处理,大部分订单早就处理完了,所以此时可能存活对象就100MB左右。

但是这里问题来了,如果“-XX:SurvivorRatio”参数默认值为8,那么此时新生代里Eden区大概占据了1.2GB内存,每个Survivor区是150MB的内存,如下图。

所以Eden区1.2GB满了就要进行Minor GC了,因此大概只需要20秒,就会把Eden区塞满,就要进行Minor GC了。

然后GC后存活对象在100MB左右,会放入S1区域内。如下图。

然后再次运行20秒,把Eden区占满,再次垃圾回收Eden和S1中的对象,存活对象可能还是在100MB左右会进入S2区,如下图。

此时JVM参数如下:

“-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M  -XX:SurvivorRatio=8”

新生代垃圾回收优化之一:Survivor空间够不够

首先在进行JVM优化的时候,第一个要考虑的问题,就是你通过估算,你的新生代的Survivor区到底够不够

按照上述逻辑,首先每次新生代垃圾回收在100MB左右,有可能会突破150MB,那么岂不是经常会出现Minor GC过后的对象无法放入Survivor中?然后岂不是频繁会让对象进入老年代?

还有,即使Minor GC后的对象少于150MB,但是即使是100MB的对象进入Survivor区,因为这是一批同龄对象,直接超过了Survivor区空间的50%,此时也可能会导致对象进入老年代。

所以其实按照我们这个模型来说,Survivor区域是明显不足的。

这里其实建议的是调整新生代和老年代的大小,因为这种普通业务系统,明显大部分对象都是短生存周期的,根本不应该频繁进入老年代,也没必要给老年代维持过大的内存空间,首先得先让对象尽量留在新生代里。

所以此时可以考虑把新生代调整为2G,老年代为1G,那么此时Eden为1.6G,每个Survivor为200MB,如下图。

这个时候,Survivor区域变大,就大大降低了新生代GC过后存活对象在Survivor里放不下的问题,或者是同龄对象超过Survivor 50%的问题。

这样就大大降低了新生代对象进入老年代的概率。

此时JVM的参数如下:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M  -XX:SurvivorRatio=8”

其实对任何系统,首先类似上文的内存使用模型预估以及合理的分配内存,尽量让每次Minor GC后的对象都留在Survivor里,不要进入老年代,这是你首先要进行优化的一个地方。


新生代对象躲过多少次垃圾回收后进入老年代?

大家都知道,除了Minor GC后对象无法放入Survivor会导致一批对象进入老年代之外,还有就是有些对象连续躲过15次垃圾回收后会自动升入老年代。

其实按照上述内存运行模型,基本上20多秒触发一次Minor GC,那么如果按照“-XX:MaxTenuringThreshold”参数的默认值15次来说,你要是连续躲过15次GC,就是一个对象在新生代停留超过了几分钟了,此时他进入老年代也是应该的。

有些博客会说,应该提高这个参数,比如增加到20次,或者30次,其实那种说法根本是不对的

因为你对这个参数考虑必须结合系统的运行模型来说,如果躲过15次GC都几分钟了,一个对象几分钟都不能被回收,说明肯定是系统里类似用@Service、@Controller之类的注解标注的那种需要长期存活的核心业务逻辑组件。

那么他就应该进入老年代,何况这种对象一般很少,一个系统累计起来最多也就几十MB而已。

所以你说你提高“-XX:MaxTenuringThreshold”参数的值,有啥用呢?让这些对象在新生代里多停留几分钟?

所以说,考虑问题,一定不要人云亦云,要结合运行原理,自己推演和思考,不同的业务系统还都是不一样的。

其实这个参数甚至你都可以降低他的值,比如降低到5次,也就是说一个对象如果躲过5次Minor GC,在新生代里停留超过1分钟了,尽快就让他进入老年代,别在新生代里占着内存了。

总之,对于这个参数务必是结合你的系统具体运行的模型来考虑。

要记住,JVM没有万能的最佳参数,但是有一套通用的分析和优化的方法。

此时JVM参数如下:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:PermSize=256M -XX:MaxPermSize=256M  -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5”

多大的对象直接进入老年代?

另外有一个逻辑是说,大对象可以直接进入老年代 ,因为大对象说明是要长期存活和使用的

比如在JVM里可能会缓存一些数据,这个一般可以结合自己系统中到底有没有创建大对象来决定。

但是一般来说,给他设置个1MB足以,因为一般很少有超过1MB的大对象。如果有,可能是你提前分配了一个大数组、大List之类的东西用来放缓存的数据。

此时JVM参数如下:

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:PermSize=256M -XX:MaxPermSize=256M  -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M”

指定垃圾回收器

同时大家别忘了要指定垃圾回收器,新生代使用ParNew,老年代使用CMS,如下JVM参数 :

“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:PermSize=256M -XX:MaxPermSize=256M  -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC”

ParNew垃圾回收器的核心参数,其实就是配套的新生代内存大小、Eden和Survivor的比例,只要你设置合理,避免Minor GC后对象放不下Survivor进入老年代,或者是动态年龄判定之后进入老年代,给新生代里的Survivor充足的空间,那么Minor GC一般就没什么问题。

然后根据你的系统运行模型,合理设置“-XX:MaxTenuringThreshold”,让那些长期存活的对象,抓紧尽快进入老年代,别在新生代里一直待着。

这样基本上一个初步的优化好的JVM参数就结合你的业务出来了。下篇文章我们继续结合案例来分析 老年代的垃圾回收和参数优化方式。


思考

可以直接去看看自己生产系统的JVM参数了,看看你的新生代、老年代、Eden和Survivor的大小

然后去估算一下你的系统运行模型:

  • 每秒占用多少内存?
  • 多长时间触发一次Minor GC?
  • 一般Minor GC后有多少存活对象?
  • Survivor能放的下吗?
  • 会不会频繁因为Survivor放不下导致对象进入老年代?
  • 会不会因动态年龄判断规则进入老年代?


相关文章
|
30天前
|
消息中间件 运维 监控
加一个JVM参数,让系统可用率从95%提高到99.995%
本文针对一个高并发(10W+ QPS)、低延迟(毫秒级返回)的系统因内存索引切换导致的不稳定问题,深入分析并优化了JVM参数配置。通过定位问题根源为GC压力大,尝试了多种优化手段:调整MaxTenuringThreshold、InitialTenuringThreshold、AlwaysTenure等参数让索引尽早晋升到老年代;探索PretenureSizeThreshold和G1HeapRegionSize实现索引直接分配到老年代;加速索引复制过程以及升级至JDK11使用ZGC。
366 82
加一个JVM参数,让系统可用率从95%提高到99.995%
|
3月前
|
缓存 算法 Java
JVM实战—4.JVM垃圾回收器的原理和调优
本文详细探讨了JVM垃圾回收机制,包括新生代ParNew和老年代CMS垃圾回收器的工作原理与优化方法。内容涵盖ParNew的多线程特性、默认线程数设置及适用场景,CMS的四个阶段(初始标记、并发标记、重新标记、并发清理)及其性能分析,以及如何通过合理分配内存区域、调整参数(如-XX:SurvivorRatio、-XX:MaxTenuringThreshold等)来优化垃圾回收。此外,还结合电商大促案例,分析了系统高峰期的内存使用模型,并总结了YGC和FGC的触发条件与优化策略。最后,针对常见问题进行了汇总解答,强调了基于系统运行模型进行JVM参数调优的重要性。
153 10
JVM实战—4.JVM垃圾回收器的原理和调优
|
3月前
|
消息中间件 算法 Java
JVM实战—5.G1垃圾回收器的原理和调优
本文详细解析了G1垃圾回收器的工作原理及其优化方法。首先介绍了G1通过将堆内存划分为多个Region实现分代回收,有效减少停顿时间,并可通过参数设置控制GC停顿时长。接着分析了G1相较于传统GC的优势,如停顿时间可控、大对象不进入老年代等。还探讨了如何合理设置G1参数以优化性能,包括调整新生代与老年代比例、控制GC频率及避免Full GC。最后结合实际案例说明了G1在大内存场景和对延迟敏感业务中的应用价值,同时解答了关于内存碎片、Region划分对性能影响等问题。
|
7月前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
179 58
|
6月前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
7月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
25天前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
183 55
|
2月前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
104 6
|
3月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
216 29
JVM简介—1.Java内存区域
|
5月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
783 166