第四章:JVM 堆内存与栈内存的启动参数配置

说实话,很多新手把启动参数想得太复杂了。其实 JVM 的内存管理,核心就两块——堆(Heap)和栈(Stack)。搞懂了这两个区域怎么配,你就掌握了 80% 的调优能力。

我记得刚入行那会儿,有个同事把 -Xms 和 -Xmx 设成了不一样的值,结果线上服务频繁 Full GC。他查了两天没找到原因,我过去看了一眼配置,直接说:「你把这两个值设成一样试试。」问题立马解决了。嗯,这就是堆内存参数的基本功。

4.1 堆内存参数:-Xms 与 -Xmx

堆是 JVM 管理对象的主要区域。几乎所有 new 出来的对象都住在这里。

  • -Xms:初始堆大小。JVM 启动时就分配这么多。
  • -Xmx:最大堆大小。JVM 最多能用到这么多。

核心原则:生产环境建议将 -Xms 和 -Xmx 设为相同值。这样可以避免运行时频繁扩容和缩容,减少系统开销。

# 设置堆内存为 2GB,初始和最大一致
java -Xms2g -Xmx2g -jar myapp.jar

为什么我强调要设成一样?因为 JVM 在堆扩容时,会触发 STW(Stop-The-World)暂停。你想想看,如果初始只有 512MB,高峰期突然要扩展到 2GB,那一次暂停可能就几百毫秒。对于高并发服务,这就是灾难。

参数 默认值(JDK 8) 建议值 说明
-Xms 物理内存的 1/64 与 -Xmx 相同 初始堆大小,避免动态调整
-Xmx 物理内存的 1/4 不超过物理内存的 70% 最大堆大小,留出系统和其他进程空间

注意:-Xmx 不要超过物理内存的 70%。我曾经见过有人把 8GB 机器设成 -Xmx7g,结果操作系统自己没内存用了,直接 OOM Killer 把 Java 进程干掉了。留点余量给系统,没错的。

4.2 栈内存参数:-Xss

栈是每个线程私有的内存区域。它存储局部变量、方法调用栈帧等信息。每个线程都有自己的栈。

参数 -Xss 用来设置每个线程栈的大小。默认值因平台而异,一般是 1MB(Linux x64)或 512KB(Windows)。

# 设置每个线程栈大小为 512KB
java -Xss512k -jar myapp.jar

什么时候需要调大?如果你的代码有深层递归调用,或者方法调用链特别长,栈空间不够就会抛出 StackOverflowError。

什么时候需要调小?如果你的应用需要创建大量线程(比如 Web 服务器),每个线程的栈太大,总内存会迅速耗尽。举个例子:-Xss1m 时,1000 个线程就要 1GB 栈内存;如果改成 -Xss256k,同样线程数只需要 256MB。

我的经验:对于大多数业务应用,256KB 到 512KB 的栈空间就足够了。除非你写的是递归算法或者深度解析器,否则别轻易调大。我曾经接手过一个项目,默认 -Xss2m,线上 3000 个线程直接吃掉了 6GB 内存,改成 256k 后内存占用降到 768MB,性能反而更好了。

4.3 堆内存的细分区域

堆不是一整块。它被分成了几个区域,每个区域有独立的参数控制。了解这些,你才能精准调优。

JVM 堆内存结构 年轻代 (Young Generation) -XX:NewSize / -XX:MaxNewSize 默认占堆的 1/3 Eden -XX:SurvivorRatio=8 S0 From S1 To 老年代 (Old Generation) -XX:NewRatio=2 默认占堆的 2/3 存放长期存活的对象 Major GC / Full GC 发生区域 元空间 (Metaspace) — 不在堆内,使用本地内存 -XX:MetaspaceSize / -XX:MaxMetaspaceSize 对象先分配到 Eden,经过 Minor GC 后进入 S0/S1,多次存活后晋升到老年代

4.4 年轻代与老年代比例参数

堆内部的比例控制,直接影响 GC 频率和停顿时间。

-XX:NewRatio

控制老年代与年轻代的比例。默认值是 2,表示老年代是年轻代的 2 倍。

# 老年代:年轻代 = 1:1
-XX:NewRatio=1

# 老年代:年轻代 = 3:1
-XX:NewRatio=3

-XX:SurvivorRatio

控制 Eden 区与 Survivor 区的比例。默认值是 8,表示 Eden : S0 : S1 = 8 : 1 : 1。

# Eden:S0:S1 = 6:1:1
-XX:SurvivorRatio=6

我个人的习惯:对于大多数 Web 应用,保持默认的 NewRatio=2 和 SurvivorRatio=8 就够用了。除非你的应用创建了大量临时对象(比如高频交易系统),才需要调大年轻代比例。我曾经优化过一个报表系统,把 NewRatio 从 2 改成 1,年轻代翻倍,Minor GC 频率从每秒 5 次降到每秒 1 次,吞吐量提升了 30%。

4.5 元空间参数:MetaspaceSize 与 MaxMetaspaceSize

JDK 8 开始,永久代(PermGen)被元空间(Metaspace)取代。元空间使用本地内存,不再受堆大小限制。

参数 作用 默认值
-XX:MetaspaceSize 元空间触发 Full GC 的阈值 约 20.8MB(依赖平台)
-XX:MaxMetaspaceSize 元空间最大大小 无上限(仅受本地内存限制)
# 设置元空间初始阈值和最大大小
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

避坑指南:我曾经遇到过一个 Spring Boot 应用,默认没设 MaxMetaspaceSize,结果因为动态类加载(CGLIB 代理)把元空间撑到了 2GB,导致系统内存不足。建议生产环境一定要设置 MaxMetaspaceSize,防止内存泄漏拖垮整个机器。

4.6 实战:一个完整的启动参数示例

把上面这些参数组合起来,就是一个生产级别的 JVM 启动配置。我拿一个 4GB 内存的服务器举例:

java -Xms2g -Xmx2g \
     -Xss256k \
     -XX:NewRatio=2 \
     -XX:SurvivorRatio=8 \
     -XX:MetaspaceSize=128m \
     -XX:MaxMetaspaceSize=256m \
     -jar myapp.jar

这个配置的含义:

  • 堆固定 2GB,避免动态调整
  • 每个线程栈 256KB,支持更多并发线程
  • 年轻代约 670MB,老年代约 1.33GB
  • 元空间上限 256MB,防止内存泄漏

记住:没有万能参数组合。每个应用都有自己的特点。上面的配置是一个不错的起点,但你需要通过压测和监控来微调。我每次上线新服务,都会先跑一周的 GC 日志分析,再决定要不要调整比例。

嗯,堆和栈的参数就这些。说白了,你只要记住 -Xms/-Xmx 控制堆大小,-Xss 控制栈大小,NewRatio 和 SurvivorRatio 控制内部比例,MetaspaceSize 控制元空间。把这些搞明白,JVM 内存这块你就出师了。