Java 内存模型与 JVM 调优:堆、栈与 GC 参数怎么配
区分堆、虚拟机栈、方法区等运行时区域,说明对象在堆中的分配晋升路径与-Xss、-Xms/-Xmx、新生代、元空间等参数配置,并给出按场景选择GC回收器的4G堆调优模板。
学完这篇,你能分清 JVM 里堆、栈、方法区各自装什么,并拿到一套可以直接抄改的内存与 GC 参数模板。
第一步:先把运行时数据区对号入座
调优调错地方的根源,往往是没分清这几个区:
- 堆(Heap):线程共享,存对象实例和数组,是 GC 主战场。参数
-Xms(初始)、-Xmx(最大)。 - 虚拟机栈:线程私有,每个方法调用压一个栈帧,放局部变量表、操作数栈、返回地址。深度由
-Xss决定。 - 方法区 / 元空间:JDK 8 起改用本地内存,存类元信息、运行时常量池。参数
-XX:MetaspaceSize、-XX:MaxMetaspaceSize。 - 程序计数器:线程私有,记录当前字节码行号,是唯一不会 OOM 的区域。
- 本地方法栈:给 native 方法用。
注意:JDK 8 之后没有永久代(PermGen),继续写
-XX:PermSize、-XX:MaxPermSize不会生效,JDK 9+ 甚至直接启动报错,要改用-XX:MetaspaceSize。
第二步:搞清对象在堆里的流转路径
新对象先在 Eden 分配(走 TLAB 快速分配);Minor GC 后存活对象进 Survivor,年龄 +1;年龄到 -XX:MaxTenuringThreshold(默认 15),或者 Survivor 中同年龄对象总和超过 -XX:TargetSurvivorRatio(默认 50%),就晋升老年代。超大对象直接进老年代,门槛是 -XX:PretenureSizeThreshold(单位字节,只对 Serial / ParNew 有效)。
理解这条路径,你才知道为什么"老年代涨得快"通常意味着新生代太小或对象晋升太早,而不是该去调老年代。
第三步:栈参数怎么定
-Xss 决定单线程栈大小,Linux x64 默认约 1M。经验公式:最大线程数 ≈ 可用内存 / Xss。
- 递归深、栈帧大 → 适当调大,如
-Xss1m或-Xss2m; - 需要开几千个线程 → 调小到
-Xss256k或-Xss512k。
注意:
-Xss调太小会让正常递归提前抛StackOverflowError;调太大则在创建大量线程时更容易抛OutOfMemoryError: unable to create native thread——这个错误不是堆不够,别去加-Xmx。
第四步:堆参数这样配
核心三条原则:
-Xms和-Xmx设成同一个值,避免运行期反复扩容收缩带来的抖动。- 新生代显式指定:
-Xmn直接给大小,或用-XX:NewRatio=2(老年代:新生代 = 2:1)。 -XX:SurvivorRatio=8表示 Eden:S0:S1 = 8:1:1,一般不用改。
新生代占堆比例的经验值:低延迟 Web 服务取 1/2 左右,批处理/吞吐型取 1/3。
一份 4G 堆的模板:
java -Xms4g -Xmx4g -Xmn1536m -Xss512k \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump \
-jar app.jar
第五步:挑一个匹配场景的回收器
| 回收器 | 开启参数 | 适用场景 |
|---|---|---|
| Serial / Serial Old | -XX:+UseSerialGC | 单核、小内存、客户端 |
| Parallel Scavenge + Parallel Old | -XX:+UseParallelGC | 批处理、离线计算,吞吐优先 |
| CMS | -XX:+UseConcMarkSweepGC | JDK 8 老项目,JDK 14 已移除 |
| G1 | -XX:+UseG1GC | JDK 9+ 默认,堆 4G 以上 Web 服务 |
| ZGC | -XX:+UseZGC | 超大堆、要求停顿 < 10ms |
常用细化参数:
- Parallel:
-XX:MaxGCPauseMillis=200、-XX:GCTimeRatio=99 - CMS:
-XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly(防碎片可加-XX:+UseCMSCompactAtFullCollection) - G1:
-XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=45
注意:别迷信"越新越好"。堆小于 4G 的普通服务,Parallel 的吞吐经常比 G1 更好;CMS 在 JDK 9 已废弃、JDK 14 移除,新项目不要选。
第六步:打开 GC 日志,用数据说话
JDK 8:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/gc.log \
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M
JDK 9+:
-Xlog:gc*,gc+heap=info:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=20M
日志重点看四件事:Minor GC 频率、单次停顿毫秒数、Full GC 次数是否持续增长、老年代回收后残留是否越来越高(后者是内存泄漏信号)。
排查工具:
jstat -gcutil <pid> 1000 # 各区使用率与 GC 次数
jmap -histo:live <pid> | head -30 # 对象占用排行
jstack <pid> > stack.txt # 线程栈,查死锁/卡顿
注意:
jmap -dump:live会触发一次 STW,大堆生产环境慎用。更稳妥的做法是提前挂上-XX:+HeapDumpOnOutOfMemoryError,让 JVM 在 OOM 时自动落盘。
第七步:容器里别让 JVM 看错内存
JDK 8u191+ 默认开启 UseContainerSupport,容器内建议别再硬编码 -Xmx:
-XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=70.0
更早的 JDK 8 版本需要 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap,否则 JVM 会读到宿主机总内存,直接把容器撑爆 OOMKilled。
注意:堆只占进程内存的一部分,元空间、线程栈、直接内存、JVM 自身开销都要留出空间,
MaxRAMPercentage给 70% 左右比较安全,别给到 90%。
小结
- 堆存对象、栈存方法调用、元空间存类信息,
-Xss管栈、-Xms/-Xmx管堆、MetaspaceSize管元空间,别串台。 -Xms = -Xmx,新生代显式指定,SurvivorRatio默认即可。- 回收器按场景选:批处理 Parallel,Web 低延迟 G1 或 ZGC,老项目 CMS 只做维护。
- 先开 GC 日志 +
jstat观察,再动参数;不观测就调参等于蒙。 - 容器环境用
MaxRAMPercentage,并给非堆内存留 30% 余量。
转载请注明出处,版权归原作者所有。
正式会员
认证极客
钻石卡会员






