【JVM】九、案例实战:JVM栈内存与永久代大小又该如何设置
本文探讨如何根据这个并发量来估算每秒钟请求对内存空间的占用,进而得出整个系统运行期间的JVM内存运转模型
1、前文回顾
上一篇文章通过案例分析,向大家介绍了在准备上线新系统时,如何根据预估的业务量和访问量来推算系统每秒的并发量。接下来,我们将探讨如何根据这个并发量来估算每秒钟请求对内存空间的占用,进而得出整个系统运行期间的JVM内存运转模型。
在得到这个JVM内存运转模型后,我们需要在系统上线前选择一个合适的机器配置,包括所需的内存大小以及为JVM堆内存空间分配合理的大小。这实际上是一项非常基本的技能,因为对于大型互联网公司的工程师来说,每次上线新系统都可能面临巨大的访问压力。
因此,掌握如何合理预估内存压力并选择相应的机器配置以及分配合适的内存大小是至关重要的。希望大家能够熟练掌握这一技能。
2、探究不合理内存设置引发的灾难故事
在上一篇文章中,我们讨论了一个正面的例子,即如何合理地设置内存大小。今天,我们将讨论一个反面的不合理设置内存大小导致的问题。
为了便于大家理解,我们仍然以支付系统作为案例来说明,其实思路是一样的。假设我们现在有一个前提,就是支付系统因为没有经过合理的内存预估,所以直接选择了一台2核4G的虚拟机来部署线上系统,而且只使用了一个机器。
然后,线上JVM给的堆内存大小,仅仅只有1G。扣除老年代之后,新生代实际上只有几百MB的内存空间。大家看下图。
我们继续使用昨天讨论的业务压力模型,即每天处理100万笔交易。在高峰期,每秒大约需要处理100笔支付交易。对于每笔交易,我们需要创建核心的支付订单对象,每个对象占据约500字节的内存空间。因此,在高峰期,每秒钟总共会创建出大约50KB的支付订单对象。
由于每笔交易需要1秒的处理时间,这100个支付订单对象在新生代中的存在时间为1秒,期间这些对象会被引用,因此无法被垃圾回收。
之前我们提到过一个全局预估的思路,即从核心的支付订单对象扩展到系统中的其他对象。按照这个思路,我们可以将内存占用扩大10倍到20倍。假设我们选择扩大20倍,那么在1秒内,系统会创建大约1MB的无法立即回收的对象。
综上所述,根据业务压力模型和全局预估思路,我们可以预见在高峰期,系统会在很短的时间内创建大量无法立即回收的对象,这可能会对系统的内存管理造成一定的压力。
3、当瞬间访问量翻十倍,我们该怎么办?
根据对内存压力的估算,对于你这样规模较小的新生代,在系统正常运行的情况下,实际上并不构成太大的问题。因为每秒新增1MB的对象,几百秒后,新生代接近满载,自然会触发Minor GC,清理掉其中99%的无用对象。
如果你的内存确实较小,最多可能会发现系统每隔几分钟会出现短暂的卡顿现象,这是因为垃圾回收过程在进行中,会对系统性能产生一定影响。关于为什么垃圾回收会影响系统性能,我们将在下周的垃圾回收主题中进行详细分析。
然而,假设你的电商系统正在举办大型促销活动呢?
通常,举办大型促销活动可能会导致系统压力瞬间增大10倍,因为平时不常访问你网站的人,今天都会涌入。这时,你可能会察觉到,每秒钟支付系统的订单量不再是100笔,而是可能达到每秒钟上千笔订单。
在这种情况下,你的系统压力本身就会变得非常巨大,不仅仅是内存方面,尤其是线程资源和CPU资源,都会几乎被占满。而内存状况将更加危险。
4、深入解析为何少数请求让老年代内存压力山大
假设你每秒要处理1000笔交易,那么每秒钟系统对内存的需求可能会增加至10MB以上。为了更加精确地预测,我们可以稍微放大这个预估值,考虑到在大促期间流量可能会激增,因此我们预计每秒对内存的占用可能达到几十MB,甚至上百MB。
然而,最需要关注的问题并不仅仅在于内存占用的增加。更为严重的是,由于压力的急剧上升,系统性能可能会下降,导致处理能力降低。原本1秒内可以处理完的1000笔交易,现在可能需要几秒,甚至几十秒才能处理完毕。此时我们看下图可能出现什么问题,假设你的新生代里已经积压了很多的数据,都快满了。

然后呢,此时内存里有比如几十MB的对象都被人引用着,因为少数请求突然处理的特别慢。
为什么会处理特别慢?因为压力太大,导致系统性能太差了,如下图。

在这个时刻,如果你决定在新生代中再次分配对象,这是否会引发一次Minor GC来回收新生代中的对象呢?
确实如此,但是尽管可能已经回收了大量的对象,仍然有一小部分几十MB的对象存在,这是由于某些请求的响应速度较慢。
然后,在不久之后,新生代可能会继续被新的对象填满,从而再次触发Minor GC。然而,那部分几十MB的对象依然存在。在这种情况下,经过多次类似的事件后,这些对象最终会被移动到老年代中。如下图。

5、老年代对象激增引发的垃圾回收风暴
设想一下,如果我们的应用程序中,有一部分请求处理的时间特别长,这可能会导致创建的对象在新生代中反复存在,而无法被及时回收。这些对象如果长时间存活,最终会被移动到老年代。随着时间的推移,当这些对象在老年代中不再被引用,它们就变成了所谓的“垃圾对象”。如果我们的应用程序经常重复这样的流程,老年代中的垃圾对象会逐渐积累。
随着老年代中垃圾对象的增多,垃圾回收器会频繁地被触发来清理这些不再使用的对象。老年代的垃圾回收通常是一个缓慢的过程,因为其设计是为了处理生命周期更长、数量更多的对象。关于老年代垃圾回收为何缓慢的具体原因,我们将在后面的讨论中详细解析。
在上述场景中,我们可以通过分析得出结论:如果内存管理不当,比如新生代的内存设置不足,会导致对象频繁地从新生代迁移到老年代。这种不断的迁移,加上老年代的垃圾回收机制,最终会引发频繁的垃圾回收操作。
这种频繁的垃圾回收操作,无疑会对系统性能产生重大影响,因为它会占用大量的CPU时间和资源,从而降低系统的响应速度和处理能力。因此,合理的内存管理和调优是至关重要的,以确保系统的高效运行和良好的用户体验。
6、反面案例总结
本文通过一个支付系统的案例来阐述,当系统内存设置过小,遭遇突发的巨大流量压力时,可能会引发性能抖动,甚至导致严重的系统问题。
在这个案例中,由于支付系统的内存设置过小,当系统突然遭遇巨大的流量压力时,会引发性能抖动。这种性能抖动会导致大量的对象在新生代被引用,但却无法被及时回收。这些对象会持续进入老年代,导致老年代的内存频繁被占满。
当老年代的内存频繁被占满时,垃圾回收机制就会被频繁触发。这不仅会消耗大量的系统资源,还可能导致系统性能下降,甚至引发系统崩溃。
因此,对于任何业务系统,我们都需要准确地预估其可能面临的压力,并合理地设置其内存大小,以避免可能出现的性能问题。
7、如何合理设置永久代大小?
关于如何合理设置永久代大小的问题是,实际上在系统刚上线时,通常没有太多可供参考的规范。一般来说,将永久代大小设置为几百MB是足够的。因为永久代主要用来存放类的信息,所以这个设置大体上可以满足需求。
然而,有些系统容易出现永久代内存溢出的情况,这时就需要进行专门的案例分析。通过分析系统的具体情况和需求,我们可以得出更合理的永久代大小设置。
8、如何合理设置栈内存大小
其实这个栈内存大小设置,一般也不会特别的去预估和设置的,一般默认就是比如512KB到1MB,就差不多够了。这就是每个线程自己的栈内存空间,用来存放线程执行方法期间的各种布局变量的。