第四章: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 堆内存的细分区域
堆不是一整块。它被分成了几个区域,每个区域有独立的参数控制。了解这些,你才能精准调优。
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 内存这块你就出师了。