一个案例理解JVM调优

一个案例理解JVM调优

近期了解到一个JVM调优的案例,让我对JVM的调优有了更深刻的认识,在此进行分享一下。而在正式开始对案例进行介绍前

1. JAVA对象的存储

HotSpot JVM 下,每个 Java 对象的存储结构通常由 对象头(Header)实例数据(Instance Data)对齐填充(Padding) 三部分组成:

① 对象头(Object Header)

对象头包含两个部分:

  • Mark Word(标记字段,8 字节)
    • 存储对象的 哈希码
    • GC 年龄
    • 锁状态(轻量级锁、重量级锁、偏向锁等)
  • Class Pointer(类型指针,8 字节)
    • 指向该对象对应的 类的元数据,用于确定对象的类型。
  • (可选)Array Length(数组长度,4 字节)
    • 如果对象是数组,还会存储数组的长度。

② 实例数据(Instance Data)

  • 这里存储了对象的成员变量(包括父类继承的字段)。
  • 变量存储顺序
    • 父类字段优先
    • 同类型的字段尽量存储在一起(JVM 会进行字段重排,以减少内存对齐的浪费)。

③ 对齐填充(Padding)

Java 对象的大小需要按照 8 字节对齐,因此 JVM 可能会填充额外的字节,以保证对象大小是 8 的倍数

2. Java 对象的存储

Java 对象在内存中的存储通常涉及 堆(Heap)、方法区(Metaspace)、栈(Stack),具体如下:

对象分配在堆上

  • 新生代:大多数对象创建后都存储在 Eden 区,短生命周期对象会很快被 GC。
  • 老年代:长生命周期对象会被移动到老年代。

对象的 Class 信息在方法区(Metaspace)

  • 对象的类型信息方法常量池存储在方法区(JDK 8 之前是 永久代,JDK 8 之后是 元空间 Metaspace)。

引用存储在栈上

  • 局部变量(对象引用)存储在 Java 线程的栈 中,实际对象在堆上。

3. Java 对象的大小计算

对象的大小由 对象头 + 实例数据 + 对齐填充 计算:

基本规则

  • 对象头固定:16 字节(非数组)或 20 字节(数组)
  • 每个字段根据类型占用 不同的字节数,如boolean和byte是1字节,char和short是2字节,int和float是4字节,long和double是4字节,引用类型是4字节(32位)或8字节(64位)

4. 业务场景说明

倘若现在存在一个电商促销的场景

  • 每秒最高可能达到2000单/s的并发量,采用4台服务器来进行负载均衡,那么每台服务器的最高并发量为 2000/4 = 500单/s
  • 每个订单会产生0.2M的内存占用,完整处理每个订单需要1s的时间,即1s后变为垃圾
  • 服务器为8G内存,假定指定最大堆大小为3G,堆内存的结构如下图所示:

根据计算,可以得知每秒可以产生100M内存空间占用,进入Elden区。那么,每8s后我们的Elden区就会填满,而由于最后一秒产生的订单尚未完全处理完全完毕,不能被回收,那么实际回收的垃圾只有700M,有100M空间无法回收,则直接存入S区。

JVM存在动态年龄机制,即如果 Survivor 空间中相同或更大年龄的对象占用的总内存 >= Survivor 空间的一半,则这些对象提前晋升到老年代,而无需等待 MaxTenuringThreshold

在该案例中,产生的100M无法回收的对象由于正好占到了S区的一半(100M),因此会提前晋升到老年代。可以判断出,每8s就会产生100M对象存入老年代,而由于old区为2G,仅需要8*20=160s即空间全部被占用,触发Full-gc,而实际上,该full-gc完全没有必要去触发。

5. JVM调优方案

针对S区和Elden区过小的问题,可以选用如下的JVM参数:

-Xms 3072M -Xmx 3072M -Xmn 2048M -XX:SurvivorRatio:7

-Xss 256K -XX:Metaspace= 128M -XX:MaxMetaspaceSize=128M

-XX:MaxTenuringThreshold=2

-XX:ParallelGCThreads=8

-XX:+UseConcMarkSweepGC

-Xms 3072M -Xmx 3072M — 将堆内存初始值和最大值设为3G,避免堆动态扩展带来的性能抖动,保证内存分配的稳定性。

-Xmn 2048M –将年轻代(Young Generation)设为2G,扩大Eden区容量,提升短期存活对象的分配效率。

-XX:SurvivorRatio:7 — 调整Eden与Survivor区的比例,避免Survivor区过小导致动态年龄晋升问题。在当前参数设置下不会触发提前晋升,对象可正常在S区存活。

-XX:MetaspaceSize=128M –固定元空间大小,避免元空间动态扩容导致Full GC(如类加载频繁的场景)

-XX:MaxTenuringThreshold=2 — 控制对象晋升老年代的年龄阈值:

  • 由于当前场景绝大多数订单处理完毕就直接销毁,对象在Survivor区可是指经历2次Minor GC后就直接晋升到老年代,避免长期占用S区。
  • 结合动态年龄机制(Survivor区足够大),减少晋升频率。

-XX:ParallelGCThreads=8 — 设置并行GC线程数为8(与CPU核数匹配),提升年轻代垃圾回收(Minor GC)效率。

-XX:+UseConcMarkSweepGC — 使用CMS(Concurrent Mark-Sweep)垃圾收集器,减少老年代GC的停顿时间:CMS通过并发标记清除,避免Full GC时长时间STW(Stop-The-World),适合对延迟敏感的电商场景。

通过上述参数调整,系统可在高并发场景下稳定运行,避免因内存分配不合理导致的性能瓶颈。

留下回复