18、案例实战:上亿请求轻松应对,看年轻代垃圾回收如何助力电商性能飞跃!

本文通过一个电商系统案例,探讨如何在大促期间优化JVM性能,尤其是在面临每秒几百订单的高峰压力时。通过对内存需求的精确预测和内存分配,以及设置合理的JVM参数,特别是新生代和Survivor区的大小,来避免新生代对象过早晋升到老年代,从而减少垃圾回收带来的性能影响。文中强调了理解系统运行模型的重要性,并提供了具体的JVM配置建议。
摘要由CSDN通过智能技术生成
18.1、背景引入

我们通常会通过案例分析,来指导大家如何在不同的场景下,预测系统的内存使用模型。我们需要合理地调整新生代、老年代、Eden和Survivor各个区域的内存大小,然后尽可能地优化参数,以减少新生代对象进入老年代的情况,让这些对象尽可能地在新生代中被回收。

在我们的讨论背景中,我们以电商系统为例。电商系统通常被拆分为多个独立的子系统进行部署,例如商品系统、订单系统、促销系统、库存系统、仓储系统和会员系统等。

我们的案例背景是一个每日处理上亿请求量的电商系统。我们可以推算,这样的系统每天会有多少活跃用户?如果我们假设每个用户平均访问20次,那么上亿的请求量大约需要500万的日活跃用户。

接下来,我们继续推算,这500万的日活跃用户中,有多少人会下订单?如果我们按照10%的付费转化率来计算,那么每天大约有50万人会下订单,也就是每天大约有50万订单。

如果这50万订单集中在每天4小时的高峰期内,那么平均每秒大约只有几十个订单。这可能会让人感觉压力并不大,因为在几十个订单的压力下,我们并不需要过多关注JVM的性能。在这种情况下,新生代的内存每秒只会被占用一部分,新生代会在很长一段时间后才会满,然后通过一次Minor GC,垃圾对象被清理掉,内存空间就被释放出来,几乎没有任何压力。

18.2、特殊的电商大促场景

如果你考虑到特殊的电商大促场景,你的想法可能会有所改变。在平常情况下,许多中小型电商平台的系统压力并不是特别大,高并发情况也相对较少,每秒几千的并发压力可能就已经算是高峰压力了。然而,一旦遇到大促场景,比如双11等,情况就会发生显著变化。

设想一下,在类似双11的节日里,零点一到,许多人都在等待大促的开始,准备疯狂购物。在这个时候,可能在大促开始的短短10分钟内,瞬间就会产生50万订单。那么,在这个时间段内,每秒可能会有接近1000的下单请求。因此,我们需要针对这种大促场景,对订单系统的内存使用模型进行深入分析。

18.3、了解大促期间您需要的机器军团规模

为了应对大促期间的瞬时下单压力,订单系统需要部署几台机器呢?

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

从机器本身的CPU资源和内存资源角度来看,抗住每秒300个下单请求是没问题的。但是问题就在于需要对JVM有限的内存资源进行合理的分配和优化,包括对垃圾回收进行合理的优化,让JVM的GC次数尽可能最少,而且尽量避免Full GC,这样可以尽可能减少JVM的GC对高峰期的系统性能的影响。

18.4、如何精确预测大促期间订单系统的内存需求?

根据背景信息,我们需要对订单系统的内存使用进行模型预估。我们假设系统每秒处理300个下单请求,这个数值与实际生产环境相近。

每个订单的处理是相对耗时的,涉及多个接口调用,因此每秒处理100~300个订单请求是合理的。我们以每个订单1KB的大小进行估算,那么300个订单将产生约300KB的内存消耗。

考虑到每个订单还会关联其他业务对象,如订单条目、库存、促销和优惠券等,通常单个订单的开销需要放大10倍至20倍。此外,除了下单操作外,订单系统还包含其他与订单相关的操作,如订单查询等,所以整体开销可以再扩大10倍。

综上所述,每秒钟的内存开销大约为:300KB × 20 × 10 = 60MB。然而,一旦这300个订单处理完毕,这些相关对象就会失去引用,进入可回收状态,因此在一秒钟后,这60MB的内存可以被视为垃圾进行回收。

大家看下图:
在这里插入图片描述

18.5、如何巧妙分配内存来提升性能?

假设我们有一台4核8G的计算机,通常我们会将JVM内存设置为4G,剩余的内存则留给操作系统等其他程序使用。在分配JVM内存时,我们可以将堆内存设置为3G,其中新生代和老年代各占1.5G。每个Java线程的虚拟机栈大小为1M,因此如果有几百个线程,大约需要几百M的内存。此外,还需要为永久代分配256M的内存。这样,基本上就分配了4G的内存。

在使用JVM时,还需要设置一些必要的参数,例如开启“-XX:HandlePromotionFailure”选项。不过,这个参数在JDK 1.6之后已经被废弃,因此在生产环境中一般不会设置这个参数。在JDK 1.6之后,只要满足以下两个条件之一,就可以直接进行Minor GC,而不需要提前触发Full GC:1. “老年代可用空间”> “新生代对象总和”;2. “老年代可用空间”> “历次Minor GC升入老年代对象的平均大小”。

因此,如果我们使用的是JDK 1.7或JDK 1.8,那么JVM参数可以保持如下设置,后面也不再加入“-XX:HandlePromotionFailure”参数:

“-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区域内。如下图。
在这里插入图片描述

当Eden区的1.2GB空间被填满时,就会触发一次Minor GC(垃圾回收)。这个过程大约需要20秒。在Minor GC执行之后,那些仍然存活的对象,其大小约在100MB左右,会被移动到S1区域,如下图。
在这里插入图片描述

此时JVM参数如下:
“-Xms3072M -Xmx3072M -Xmn1536M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:SurvivorRatio=8”

18.6、如何优雅地解决Survivor空间不足?

在对JVM进行优化时,首先需要关注的一个问题是,通过估算判断新生代的Survivor区是否足够。

根据上述逻辑,如果每次新生代垃圾回收大约占用100MB,甚至可能超过150MB,那么在Minor GC之后,对象无法放入Survivor区的情况可能会经常发生,这会导致对象频繁地进入老年代。

此外,即使在Minor GC后的对象少于150MB,但如果是100MB的对象进入Survivor区,由于这是一批同龄的对象,它们会直接超过Survivor区空间的50%,这也可能导致对象进入老年代。

因此,按照我们这个模型来看,Survivor区域显然是不足的。

在这里,建议调整新生代和老年代的大小。对于这种普通业务系统,显然大部分对象都是短生命周期的,不应该频繁进入老年代,也没有必要为老年代分配过大的内存空间。首先应该尽量让对象留在新生代中。

因此,可以考虑将新生代调整为2GB,老年代调整为1GB。这样,Eden区将为1.6GB,每个Survivor区将为200MB。如下图。
在这里插入图片描述

在这个阶段,我们可以通过增大Survivor区域的大小,有效地降低新生代垃圾回收(GC)后存活对象无法放入Survivor区域的问题,或者解决同龄对象超过Survivor区域50%的问题。这样做可以显著降低新生代对象被晋升到老年代的几率。

在这种情况下,Java虚拟机(JVM)的参数设置如下:

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

对于任何系统,首先需要做的是进行内存使用模型的预估和合理分配内存,尽量确保每次Minor GC后的对象都能留在Survivor区域,避免它们被晋升到老年代。这是优化的首要步骤。

18.7、新生代如何巧妙躲过垃圾回收进入老年代?

众所周知,当对象在Minor GC后无法放入Survivor区时,它们会被送入老年代。此外,如果某些对象连续躲过15次垃圾回收,它们也会自动晋升至老年代。

根据上述内存运行模型,通常情况下,每20多秒会触发一次Minor GC。按照默认参数“-XX:MaxTenuringThreshold”的值15,如果一个对象连续躲过15次GC,意味着它在新生代中已经停留了几分钟。在这种情况下,将其晋升至老年代是合理的。

有些博客建议提高这个参数,例如将其增加到20或30。然而,这种观点并不正确。在考虑调整该参数时,必须结合系统的运行模型。如果一个对象在几次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
18.8、究竟什么决定了对象直达老年代?

在计算机编程中,有一个逻辑概念是大对象可以直接进入老年代。这是因为大对象通常表示它们需要长期存活和使用。例如,在Java虚拟机(JVM)中,可能会缓存一些数据,这通常可以根据系统中是否创建了大对象来决定。

然而,一般来说,将大对象的阈值设置为1MB就足够了。因为超过1MB的大对象非常罕见。如果存在这样的大对象,可能是因为你提前分配了一个大数组、大List等用于存放缓存数据的数据结构。此时JVM参数如下:

-Xms3072M -Xmx3072M -Xmn2048M -Xss1M  -XX:PermSize=256M -XX:MaxPermSize=256M  -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M
18.9、正确配置JVM垃圾回收器

同时,请大家不要忘记设置垃圾回收器。对于新生代,我们使用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参数,它已经结合了你的业务需求。明天,我们将继续通过案例来分析老年代的垃圾回收和参数优化方法。

  • 40
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无法无天过路客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值