2.JVM内存划分

#jvm #内存 #gc

1. 分代模型

JVM 堆分为年轻代和老年代:

对象优先在年轻代分配,年轻代内存不够时触发 Minor GC(Young GC)。每次 GC 后还存活的对象年龄 +1,超过阈值后晋升老年代。具体的晋升规则和回收算法见 3.垃圾回收

年轻代内部结构:1 个 Eden 区 + 2 个 Survivor 区(S 0/S 1),默认比例 8:1:1。每次 GC 把 Eden + 一个 Survivor 的存活对象复制到另一个 Survivor,只有 10% 的内存闲置。

新生代内存示意

1.1 为什么是 8:1:1?

复制算法要求同时存在两个 Survivor,一个用来接收存活对象,另一个空着等下次 GC。所以 Survivor 永远有一半是闲置的,这是复制算法的固有代价。

8:1:1 的设计依据是 JVM 的经验统计:绝大多数对象在第一次 GC 时就会死亡(朝生夕死),每次 Young GC 后存活下来的对象通常只占 Eden 的 10% 以内。所以只需要一个 10% 大小的 Survivor 就能装下存活对象,Eden 尽量大,让更多对象在 Eden 里创建和死亡,减少 GC 频率。

如果 Survivor 设太大,Eden 就小了,GC 更频繁;如果 Survivor 设太小,存活对象装不下,就会提前溢出到老年代,触发 Full GC。

1.2 什么时候需要调整比例?

存活对象多,Survivor 装不下jstat -gcnew 看到 TT(实际晋升年龄)很小,比如只有 1~2,说明对象还没熬到设定年龄就被迫晋升了。这时候要调小 SurvivorRatio,让 Survivor 更大:

# 默认 8,即 Eden:S0:S1 = 8:1:1
# 改成 4,即 Eden:S0:S1 = 4:1:1,Survivor 占年轻代的 1/6 ≈ 16%
-XX:SurvivorRatio=4

存活对象很少,Survivor 长期空着:Eden 可以更大,适当调大 SurvivorRatio,减少 GC 频率。

业务有大量长生命周期对象:比如缓存、连接池,这类对象本来就要进老年代,调整 Survivor 意义不大,更应该考虑直接调大老年代或者换 G 1。

调整前先用 jstat -gcnew <PID> 观察实际的晋升情况,不要凭感觉改参数。

2. 核心 JVM 参数

参数 含义
-Xms 堆初始大小
-Xmx 堆最大大小
-Xmn 年轻代大小(剩余为老年代)
-Xss 每个线程的栈大小
-XX:MetaspaceSize MetaSpace 初始大小(JDK 8+)
-XX:MaxMetaspaceSize MetaSpace 最大大小
-XX:SurvivorRatio Eden 与 Survivor 的比例,默认 8
-XX:MaxTenuringThreshold 晋升老年代的年龄阈值,默认 15
-XX:PretenureSizeThreshold 大对象直接进老年代的大小阈值

-Xms-Xmx 建议设成一样,避免堆动态扩容带来的性能抖动。G 1 不需要手动设 -Xmn,它会自动调整新生代大小,见 3.垃圾回收#4.2 G1

3. 内存估算方法

以支付系统为例:每天 100 万笔订单,高峰 3 小时,每秒约 100 笔,分布 3 台机器,每台每秒处理 30 笔。

假设每笔订单对象约 500 字节,30 笔 = 15 KB。考虑到一次请求会创建十几种对象,扩大 10~20 倍,每秒约产生 1 MB 新对象。

4 核 8 G 机器参考配置:

-Xms3g -Xmx3g -Xmn2g -Xss1m
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
-XX:SurvivorRatio=8

2 G 年轻代,Survivor 各 100 MB,每秒 1 MB 的对象大约 100 秒触发一次 Young GC,GC 后存活对象远小于 100 MB,不会频繁晋升老年代。