⭐ 推荐:社区规则条款 V1.0

Java 内存模型与 JVM 调优:堆、栈与 GC 参数怎么配

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员
发布于 2026-08-12 17:25 ·87 浏览 ·0 回复
内容摘要

区分堆、虚拟机栈、方法区等运行时区域,说明对象在堆中的分配晋升路径与-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。

第四步:堆参数这样配

核心三条原则:

  1. -Xms 和 -Xmx 设成同一个值,避免运行期反复扩容收缩带来的抖动。
  2. 新生代显式指定:-Xmn 直接给大小,或用 -XX:NewRatio=2(老年代:新生代 = 2:1)。
  3. -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:+UseConcMarkSweepGCJDK 8 老项目,JDK 14 已移除
G1-XX:+UseG1GCJDK 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% 余量。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-530.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~