新生代/老年代参数设置
分代的理论基础
在动手设置参数之前,先理解为什么 JVM 要做分代。
一个重要的经验观察:大多数对象的生命周期很短。
统计数据显示,在典型应用中:
- 60%~80% 的对象是「朝生夕灭」的(创建后很快就不可达)
- 只有 5%~10% 的对象会存活较长时间
- 只有 1%~5% 的对象会长期存活
基于这个观察,JVM 把堆分成年轻代和老年代:
┌─────────────────────────────────────────────────┐
│ 堆(Heap) │
│ │
│ 老年代(长期存活对象) │
│ ┌───────────────────────────────────────────┐ │
│ │ 占用约 2/3 的堆 │ │
│ │ GC 频率低,但 Full GC 停顿时间较长 │ │
│ └───────────────────────────────────────────┘ │
│ │
│ 新生代(朝生夕灭对象) │
│ ┌───────────────┬────────────────────────────┐ │
│ │ Eden (8/10) │ Survivor S0 │ Survivor S1 │ │
│ │ 新对象在这分配 │ MinorGC 中转 │ │
│ └───────────────┴────────────────────────────┘ │
└─────────────────────────────────────────────────┘年轻代的 Minor GC 非常频繁,但停顿时间很短(通常几十毫秒)。老年代的 Full GC 频率低,但一旦发生停顿时间可能很长(数百毫秒甚至秒级)。
年轻代参数
年轻代大小
bash
# JDK 8 及之前:-Xmn 设置年轻代大小
java -Xmn2g MyApp
# JDK 9+:可以用比例设置
java -XX:NewRatio=2 MyApp # 老年代:年轻代 = 2:1
# 如果堆 4GB,年轻代约 1.3GBEden 与 Survivor 比例
bash
# SurvivorRatio = Eden / Survivor
# 默认值 8,表示 Eden : Survivor = 8 : 1 : 1
# 年轻代 1GB 时:Eden=800MB,S0=100MB,S1=100MB
# 如果有很多对象需要存活较长时间(但不到老年代阈值)
# 可以增大 Survivor 区
java -XX:SurvivorRatio=4 MyApp
# 年轻代 1GB:Eden=640MB,S0=180MB,S1=180MB晋升阈值
bash
# 对象经历多少次 Minor GC 后晋升老年代
# 默认值 15(JDK 8 及之前最大是 15)
java -XX:MaxTenuringThreshold=15 MyApp
# JDK 9+ 最大值变成 31
java -XX:MaxTenuringThreshold=31 MyApp
# 设置晋升阈值=1 表示对象第一次 MinorGC 后直接晋升
# 适用于大多数对象会在第一次 MinorGC 后失效的场景
java -XX:MaxTenuringThreshold=1 MyApp大对象直接进入老年代
bash
# 超过这个大小的对象直接在老年代分配
# 默认值 0,表示不启用
java -XX:PretenureSizeThreshold=1048576 MyApp
# 超过 1MB 的对象直接在老年代分配老年代参数
老年代大小
bash
# 老年代大小 = 堆大小 - 年轻代大小
# -Xmx=4g -Xmn=1g → 老年代约 3GB
java -Xmx4g -Xmn1g MyApp
# 可以用 NewRatio 间接控制
java -XX:NewRatio=3 MyApp
# 老年代:年轻代 = 3:1,堆 4GB 时年轻代 1GB,老年代 3GB老年代空间分配担保
在 Minor GC 之前,JVM 会检查老年代最大可用连续空间是否大于年轻代所有对象的总空间。如果小于,JVM 会根据 HandlePromotionFailure 参数决定是否允许担保失败。
bash
# JDK 8 Update 24 之后,HandlePromotionFailure 参数被移除
# JVM 总是假设担保会成功,老年代空间不足时直接触发 Full GC
# 担保失败的逻辑:
# 老年代最大连续空间 >= 年轻代对象总大小 → Minor GC,安全
# 老年代最大连续空间 < 年轻代对象总大小 → Full GC(担保失败)G1 的特殊参数(JDK 11+)
G1 不再使用传统的分代结构,而是把堆划分为多个 Region:
bash
# 设置 G1 Region 大小
java -XX:G1HeapRegionSize=4m MyApp
# 最小 1MB,最大 32MB,必须是 2 的幂
# 目标停顿时间
java -XX:MaxGCPauseMillis=200 MyApp # 目标最大 GC 停顿 200ms
# GC 时间占比
java -XX:GCTimeRatio=19 MyApp # GC 时间占总时间不超过 1/(1+19) = 5%调优实战
场景一:高并发 Web 服务
这类服务的特点是对象生命周期短,但请求量大。
bash
# 推荐配置
java -Xms4g -Xmx4g \ # 固定堆大小,避免运行时调整
-Xmn2g \ # 年轻代 2GB,足够容纳大量短期对象
-XX:SurvivorRatio=8 \ # 默认比例,Eden:Survivor = 8:1:1
-XX:MaxTenuringThreshold=8 \ # 略微降低晋升阈值,减少老年代压力
-XX:+UseParallelGC \ # 使用 Parallel GC,吞吐量优先
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
MyApp场景二:大数据处理/批处理
这类场景需要高吞吐量,GC 停顿可以稍长。
bash
# 推荐配置
java -Xms8g -Xmx8g \
-Xmn6g \ # 年轻代较大,提高吞吐量
-XX:SurvivorRatio=4 \ # 增大 Survivor 区,减少晋升老年代的对象
-XX:MaxTenuringThreshold=20 \
-XX:+UseParallelOldGC \ # 老年代也用并行 GC
MyApp场景三:低延迟服务(金融交易、游戏服务器)
bash
# 推荐配置
java -Xms4g -Xmx4g \
-XX:+UseG1GC \ # G1GC,平衡吞吐和延迟
-XX:MaxGCPauseMillis=100 \ # 目标最大停顿 100ms
-XX:G1HeapRegionSize=8m \ # 较大的 Region
-XX:InitiatingHeapOccupancyPercent=45 \ # 触发并发 GC 的堆使用率
MyApp场景四:容器环境(Kubernetes)
bash
# 推荐配置
java -XX:+UseContainerSupport \ # JDK 10+,让 JVM 自动感知容器资源
-XX:MaxRAMPercentage=75 \ # 堆最多使用 75% 的容器内存
-XX:InitialRAMPercentage=75 \
-XX:+UseG1GC \
MyApp注意:在容器环境中,不要用
-Xmx硬编码值,否则无法充分利用容器分配的内存。
参数计算示例
假设总堆 4GB,想设置年轻代占 1/3:
bash
# 方法一:-Xmn 直接指定
java -Xmx4g -Xmn1360m MyApp
# 方法二:-XX:NewRatio
java -Xmx4g -XX:NewRatio=2 MyApp
# NewRatio=2 表示老年代:年轻代 = 2:1
# 即年轻代 = 堆 * 1/(NewRatio+1) = 4GB * 1/3 ≈ 1.33GB
# Survivor 区大小计算
# SurvivorRatio=8,Eden:Survivor = 8:1:1
# 年轻代 1.33GB:Eden ≈ 1.07GB,每个 Survivor ≈ 133MB本节小结
分代参数设置的核心原则:
| 参数 | 作用 | 默认值 |
|---|---|---|
-Xmn / -XX:NewRatio | 年轻代大小 | 新生代约占堆的 1/3 |
-XX:SurvivorRatio | Eden/Survivor 比例 | 8(Eden: S0: S1 = 8:1:1) |
-XX:MaxTenuringThreshold | 晋升老年代的年龄 | 15(JDK 8) |
-XX:PretenureSizeThreshold | 大对象阈值 | 0(不启用) |
调优的关键是理解自己应用的内存分配模式:
- 对象短命 → 增大年轻代,降低晋升阈值
- 对象长寿 → 增大老年代
- 低延迟 → 使用 G1GC,设置目标停顿时间
- 高吞吐 → 使用 Parallel GC,增大年轻代
下一节,我们来看 对象分配过程(一般/特殊情况)。
