2.JVM内存划分
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,不会频繁晋升老年代。