本文以资深 Java 开发视角重写,参考 JDK 25 LTS / JDK 27 官方文档,讲清 JVM 内存布局、对象分配、堆 sizing、GC 原理、G1/ZGC/Shenandoah/Parallel/Serial 的取舍、GC 日志与排障,并在最后给出 SRE 视角的专业见解。
最后更新:2026-10-22
- 版本与范围
- JVM 运行时内存
- 对象分配与布局
- 堆 sizing 与容器
- GC 基础原理
- 垃圾收集器
- G1 深挖
- ZGC 深挖
- 其他收集器
- GC 选型
- GC 日志与诊断工具
- 调优方法论
- 常见故障
- SRE 视角
- 企业实践
- 附录
版本与范围
- JDK 25 LTS:2025-09 GA,生产基线
- JDK 27:2026-09 GA,当前最新非 LTS
- 默认收集器:server-class 机器上是 G1,否则 Serial
- ZGC 从 JDK 23 起默认分代(JEP 474);JDK 21 起分代 ZGC 可用(JEP 439)
- 本文以 HotSpot 为主,OpenJDK 发行版通用
GC 选型没有“最佳”,只有“匹配业务”:
- 吞吐优先:Parallel
- 通用平衡:G1(默认)
- 极低延迟:ZGC / Shenandoah
- 小堆/单核:Serial
- 性能测试/特殊场景:Epsilon
JVM 运行时内存
+--------------------------------------------------+ |
Heap
- 所有线程共享
- 存放对象实例和数组
- 分代:Young(Eden/Survivor)、Old
- GC 的主要区域
- 由
-Xms/-Xmx控制
Metaspace
- 类元数据、方法、常量池、注解等
- JDK 8 起取代 PermGen
- 默认不设上限,受本机内存限制
-XX:MetaspaceSize初始阈值,-XX:MaxMetaspaceSize上限- 类加载泄漏会表现为 Metaspace 持续增长
Code Cache
- JIT 编译后的机器码
-XX:ReservedCodeCacheSize、-XX:InitialCodeCacheSize- 满了会停止 JIT,性能下降
- 关注
CodeCache使用率和编译线程
Thread Stack
- 每个线程私有
- 存放栈帧、局部变量、操作数栈
-Xss控制,Linux x64 默认通常 1MB- 线程数 × 栈大小是常被忽略的 native 内存
Direct Memory
ByteBuffer.allocateDirect、Netty、NIO- 不走 Heap,但受
-XX:MaxDirectMemorySize限制 - 未设置时默认上限约等于最大堆
- 泄漏表现为堆正常但进程 RSS 持续上涨
Native Memory
- JNI、zip、crypto、GC 元数据、线程栈、mmap
-XX:NativeMemoryTracking=summary+jcmd VM.native_memory排查- 容器 OOM 往往不是 Heap 超限,而是 Heap + Metaspace + Direct + Thread + Native 总和超限
对象分配与布局
对象布局
HotSpot 对象通常包括:
+----------------+----------------+---------------------+ |
- 对象按 8 字节对齐
- 压缩指针(Compressed Oops)在堆 < 32GB 时常用
- 数组多一个 length 字段
- 对象头大小影响小对象内存占用
- 字段重排(field layout)由 JVM 优化
分配路径
- TLAB:线程本地分配缓冲区,绝大多数对象在 TLAB 内指针碰撞分配
- 慢路径:TLAB 不足,申请新 TLAB 或直接在 Eden 分配
- 大对象:超过阈值直接进入 Old 或 Humongous Region(G1)
- 逃逸分析:JIT 可能做标量替换/栈上分配,减少堆分配
TLAB 的意义:
- 避免多线程竞争堆指针
- 提升分配速度
- TLAB 大小自适应,关注
-XX:TLABSize、-XX:TLABWasteTargetPercent
分配速率
- 分配速率高会加快 Young GC
- 晋升速率高会加快 Old GC
- 大对象/大数组会造成碎片和 Humongous 压力
- 短生命周期对象多,G1/ZGC 的分代假设更有效
堆 sizing 与容器
默认值
Oracle 文档的 ergonomics:
- 初始堆:物理内存的 1/64
- 最大堆:物理内存的 1/4
- server-class:2+ CPU 且内存 ≥ 1792MB
- server-class 默认 G1,否则 Serial
这些默认值不适合生产容器,必须显式设置。
容器
-XX:+UseContainerSupport默认开启(JDK 10+,8u191+)- JVM 读取 cgroup 内存限制
-XX:MaxRAMPercentage默认 25%-XX:InitialRAMPercentage默认 1.5625%-XX:MinRAMPercentage用于小堆-Xmx会覆盖百分比-XX:ActiveProcessorCount可覆盖 CPU 数
推荐:
-XX:MaxRAMPercentage=75.0 |
为什么不是 100%:
- Metaspace
- Direct Memory
- 线程栈
- Code Cache
- GC 自身结构
- JNI/native
- 容器 page cache 也会计入 cgroup
堆大小公式
粗略估算:
容器内存 ≥ 堆 × 1.3 ~ 1.5 + 固定开销 |
更准确:
容器 limit ≥ Heap + Metaspace + Direct + Threads×Xss + CodeCache + GC overhead + Native + 余量 |
- 堆不是越大越好:大堆会增加 GC 停顿/并发回收压力
- 小堆会增加 GC 频率和 CPU 开销
- 先测 live set,再定堆大小
-Xms=-Xmx可减少堆伸缩,但会提高常驻内存
容器 OOM
OOMKilled/ exit 137:cgroup 内存超限- JVM 堆 OOM:
java.lang.OutOfMemoryError,进程可能不退出 - 两者排查路径不同
- 看
docker inspect的OOMKilled、/sys/fs/cgroup/memory.events、dmesg
GC 基础原理
什么是垃圾
从 GC Roots 不可达的对象:
- 栈帧局部变量
- 静态变量
- JNI 引用
- 活动线程
- 类加载器/常量
GC 动作
- Mark:标记存活对象
- Sweep:回收未标记对象
- Copy:把存活对象复制到新区域
- Compact:压缩,减少碎片
- Evacuate:G1 的复制/疏散
STW
Stop-The-World:暂停应用线程完成某些阶段。暂停时间直接影响尾延迟。
三色标记
- 白:未访问
- 灰:已访问,引用未扫描完
- 黑:已访问,引用扫描完
并发标记要解决“对象消失”问题:
- SATB(Snapshot-At-The-Beginning):G1 使用,记录标记开始时的引用快照
- 增量更新:CMS 历史方案
写屏障与读屏障
- 写屏障:对象引用写入时记录,维护卡表/记忆集
- 读屏障:读取引用时处理,ZGC/Shenandoah 使用
- 屏障有 CPU 开销,是并发收集器吞吐下降的原因之一
分代假设
- 大多数对象朝生夕死
- 存活越久的对象越可能继续存活
分代收集利用这个假设,降低每次 GC 的扫描范围。
Card Table 与 Remembered Set
- 卡表:把堆分成固定大小 card(G1 常用 512B),记录跨代引用
- Remembered Set:记录从其他 Region 指向当前 Region 的引用
- 避免每次 Young GC 扫描整个 Old
- 记忆集占用内存,跨代引用多时开销大
垃圾收集器
Serial
- 单线程 STW
- 适合小堆、单核、客户端
-XX:+UseSerialGC- 简单、开销小
Parallel
- 多线程 STW,吞吐优先
- 分代,Young/Old 并行
- 适合批处理、离线计算、可接受停顿
-XX:+UseParallelGC- 关注
-XX:ParallelGCThreads、MaxGCPauseMillis
G1
- 默认收集器
- Region 化、分代、并发标记、增量回收
- 目标:可预测停顿 + 较高吞吐
- 适合大多数服务端应用
-XX:+UseG1GC
ZGC
- 并发标记/转移/重定位
- 停顿通常 < 1ms,几乎与堆大小无关
- 适合低延迟、大堆
-XX:+UseZGC- 需要更多 CPU 和内存余量
Shenandoah
- 低延迟并发压缩收集器
- OpenJDK 发行版支持
-XX:+UseShenandoahGC- 适合对停顿敏感的场景,生态/调优资料少于 G1/ZGC
Epsilon
- 不做垃圾回收,只分配
- 用于性能测试/短生命周期任务
-XX:+UseEpsilonGC- 堆满即 OOM,不适合生产常驻服务
G1 深挖
Region
G1 把堆分成大小相等的 Region:
+----+----+----+----+----+----+ |
- Region 大小 1–32MB,2 的幂
- 默认目标约 2048 个 Region
- Eden/Survivor/Old 是逻辑角色,不是连续空间
- Humongous:对象 ≥ Region 的一半,分配在连续 Humongous Region
- Humongous 回收代价高,容易造成碎片和 Full GC
收集周期
- Young-Only 阶段:普通 Young GC,对象晋升
- Old 占用达到 IHOP,触发 Concurrent Start
- 并发标记
- Remark
- Cleanup
- Space-Reclamation:Mixed GC,回收部分 Old Region
- 回到 Young-Only
关键参数:
-XX:MaxGCPauseMillis=200:目标,不是保证-XX:G1HeapRegionSize-XX:InitiatingHeapOccupancyPercent=45(Adaptive IHOP 开启时是初始值)-XX:G1NewSizePercent、-XX:G1MaxNewSizePercent-XX:G1MixedGCLiveThresholdPercent-XX:G1HeapWastePercent-XX:G1MixedGCCountTarget-XX:G1ReservePercent-XX:+UseStringDeduplication
记忆集与卡表
- 每个 Region 有 Remembered Set
- 卡表 512B
- 跨 Region 引用多时,记忆集开销大
- 大量跨代引用会导致 GC 变慢
Evacuation Failure
- 目标 Region 不够,对象无法复制
- 日志:
Evacuation Failure: Allocation/Pinned Pinned:JNIGetPrimitiveArrayCritical/GetStringCritical等导致对象被钉住- 可能触发 Full GC
- 处理:增大堆/Region、减少大对象、避免长时间 JNI critical、调
G1ReservePercent
Full GC
- G1 的兜底,整堆压缩
- 通常意味着晋升失败、Humongous 碎片、并发标记跟不上
- 频繁 Full GC 要看 allocation rate、live set、IHOP、Region 大小
- 不要用
-XX:+DisableExplicitGC掩盖问题,先查System.gc来源
日志
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=50m |
关注:
- Young/Mixed/Full GC 频率
- 每次停顿时间
- 回收前后堆占用
- Humongous 分配
- Evacuation Failure
- 并发标记周期
- 晋升速率
ZGC 深挖
特点
- 并发标记、并发转移、并发重定位
- 停顿通常 < 1ms
- 停顿与堆大小基本无关
- 支持几百 MB 到 16TB 堆
- JDK 23 起默认分代
- 需要更多 CPU 余量
核心机制
- Colored Pointers:64 位指针中带元数据位
- Load Barrier:读取引用时修正/标记
- Store Barrier:分代 ZGC 维护记忆集和标记
- 并发转移:对象在应用运行时移动
- Region:ZGC 自己的 Region 管理
分代 ZGC 的目标:
- 降低分配停顿风险
- 降低堆内存开销
- 降低 GC CPU 开销
- 保持 <1ms 停顿
堆 sizing
-Xmx是最重要的参数- 需要容纳 live set + 分配余量
- 分配速率越高,需要越多余量
-XX:SoftMaxHeapSize软上限,避免过早增长-XX:-ZUncommit禁止归还未用内存-XX:ZUncommitDelay=300归还延迟-Xms=-Xmx+-XX:+AlwaysPreTouch可降低运行时缺页/归还未内存的抖动
大页
echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages |
- 大页提升 TLB 命中,降低延迟
- 需要 root 配置 huge page pool
- 容器环境要确认宿主/容器 huge page 可见性
适用
- 低延迟 API/交易/网关
- 大堆且停顿敏感
- 能接受一定吞吐下降和更高 CPU/内存开销
- 不适合 CPU 已经很紧张、吞吐优先的批处理
其他收集器
Parallel
- 吞吐优先,STW
- 适合离线批处理、ETL、可接受秒级停顿
- 调
-XX:ParallelGCThreads、-XX:MaxGCPauseMillis - 大堆 Full GC 可能很长
Shenandoah
- 并发压缩,低延迟
- 适合对停顿敏感、使用 OpenJDK 的场景
- 调优资料和生态不如 G1/ZGC
- 关注
-XX:ShenandoahGCHeuristics等发行版特定参数
Serial
- 小堆、单核、容器边车/工具
- 简单可靠
- 不适合大堆服务端
GC 选型
| 场景 | 推荐 | 理由 |
|---|---|---|
| 通用 Web/API | G1 | 默认、平衡 |
| 低延迟交易/网关 | ZGC | 停顿 <1ms |
| 大堆且停顿敏感 | ZGC | 停顿与堆大小无关 |
| 批处理/离线 | Parallel | 吞吐高 |
| 小堆/单核 | Serial | 开销小 |
| 性能测试 | Epsilon | 排除 GC 干扰 |
| OpenJDK 低延迟 | Shenandoah | 并发压缩 |
选型步骤:
- 明确 SLO:吞吐、P50/P99/P999 延迟、可用性
- 先跑默认 G1,采集基线
- 有明确证据再换收集器
- 一次只改一个变量
- 用生产相似流量压测
- 灰度发布,观察 GC 日志和业务指标
- 保留回滚
不要凭“网上说 ZGC 最快”就换。ZGC 的 CPU/内存成本、GC 线程、容器限制都要评估。
GC 日志与诊断工具
启动参数
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=50m |
jcmd
jcmd <pid> GC.heap_info |
jstat
jstat -gcutil <pid> 1000 |
关注:
- Eden/Survivor/Old 使用率
- YGC/FGC 次数和时间
- GCT 总 GC 时间
- 最近 GC 原因
jmap / jstack
jmap -histo:live <pid> |
JFR
- 低开销持续采集
- 记录 GC、分配、锁、IO、线程、异常
- 用 JMC 或
jfr工具分析 - 生产建议常开或按需开启
NMT
java -XX:NativeMemoryTracking=summary ... |
用于区分 Heap、Metaspace、Thread、Code、GC、Internal、Other。
OS 工具
top/ps:RSS、CPUpmap/smaps:进程内存映射perf:CPU 热点async-profiler:CPU/分配/锁火焰图dmesg/journalctl:OOM killer、cgroupdocker inspect/cgroup:OOMKilled
调优方法论
步骤
- 定义目标:P99 延迟、GC 停顿、吞吐、内存上限
- 建立基线:默认参数、生产相似流量、至少 30 分钟
- 采集证据:GC 日志、JFR、NMT、heap histogram、线程栈
- 提出假设:例如“Humongous 分配导致 Mixed GC 频繁”
- 一次改一个变量:堆、Region、IHOP、收集器
- 验证:对比 P99、GC 时间、CPU、RSS
- 灰度/回滚:小流量验证,保留旧参数
常见调优方向
- 延迟高:先看 GC 停顿、 safepoint、锁、下游
- 吞吐低:看 GC 频率、CPU、分配速率
- 内存高:看 live set、Metaspace、Direct、线程栈、Native
- Full GC 频繁:看晋升失败、Humongous、IHOP、堆不足
- 容器 OOM:看总内存,不只看 Heap
不要做的事
- 复制网上参数
- 同时改多个参数
- 用
-Xmn硬编码 Young 大小(G1 不推荐) - 用
-XX:+DisableExplicitGC掩盖System.gc - 只看 GC 次数,不看停顿和业务影响
- 用
nobarrier/关闭一致性换性能
常见故障
频繁 Full GC
原因:
- 堆不足/live set 太大
- 晋升失败
- Humongous 碎片
- 并发标记跟不上分配速率
- 显式
System.gc - Metaspace/ClassLoader 泄漏
处理:
- 看 GC 日志和 heap histogram
- 增大堆/Region,减少大对象
- 调 IHOP/Reserve
- 排查泄漏
- 必要时换 ZGC
长停顿
- Young/Mixed GC 停顿超目标
- 大对象/Humongous
- 记忆集过大
- 线程数过多/CPU 争抢
- safepoint 长时间等待
- 换 ZGC/Shenandoah 评估
Metaspace OOM
- 动态生成类(CGLIB/ByteBuddy/脚本)
- ClassLoader 泄漏
- 热部署/OSGi
- 设置
MaxMetaspaceSize并监控 - 用
jcmd GC.class_histogram/jmap -clstats
Direct Memory 泄漏
- Netty
ByteBuf未释放 - NIO DirectBuffer
- 堆正常但 RSS 上涨
-XX:MaxDirectMemorySize限制- NMT 看 Internal/Other
线程泄漏
- 线程池未关闭
- 阻塞/死锁
- 线程数 × Xss 占 native
jstack、Thread.print、JFR
容器 OOM
- exit 137/OOMKilled
- 堆 + 非堆 + native 超 cgroup
MaxRAMPercentage设太高- 线程/直接内存/元空间增长
- 用
memory.events、dmesg、NMT 排查
JVM 堆 OOM
Java heap spaceGC overhead limit exceededMetaspaceDirect buffer memoryunable to create native thread
不同 OOM 类型对应不同排查路径。
SRE 视角
以下是从 SRE 角度对 JVM 服务的专业见解。
JVM 是服务的一部分,不是黑盒
- GC 停顿会直接变成用户延迟
- Heap/非堆/native 会直接影响容器 OOM
- JVM 参数是配置,要版本化、评审、灰度
- 不要把 JVM 调优当成“上线后再说”
SLI/SLO
建议监控:
- 请求成功率
- P50/P95/P99/P999 延迟
- GC 停顿 P99/最大值
- GC 时间占比
- Heap after GC / live set
- 分配速率、晋升速率
- Full GC 次数
- Metaspace/Direct/Thread/CodeCache
- 容器 RSS 与 limit
- 重启次数、OOMKilled
SLO 要包含 GC 影响:
- 不要用“排除 GC 的延迟”自欺欺人
- GC 导致的超时也算错误预算
- 尾延迟比平均值更重要
告警
- Full GC 频率/时长超阈值
- Heap after GC 持续 > 70%–80%
- 容器 RSS 接近 limit
- Metaspace/Direct 持续增长
- GC 停顿导致 SLO burn
- 频繁重启/OOMKilled
- safepoint 时间异常
告警要可行动,附 Dashboard、GC 日志、runbook。
容量
- 按 live set + 分配速率 + 余量定堆
- 预留 non-heap/native/线程栈
- ZGC 需要更多 CPU/内存余量
- G1 大堆要关注停顿和记忆集
- 容器 limit 不是“越大越好”,要成本约束
- 定期 rightsizing,避免长期超配
变更
- JVM 参数进 Git,按服务/环境管理
- 一次只改一个变量
- 灰度/金丝雀发布
- 对比 GC 日志、P99、错误率、RSS
- 保留回滚参数
- 升级 JDK 要回归 GC 行为和依赖兼容
可观测性
- GC 日志集中采集,带 pod/服务标签
- JFR 按需/持续采样
- Micrometer/OTel 暴露 JVM 指标
- Dashboard 分服务、实例、版本
- 关联 GC 停顿与请求延迟/trace
- 保留 heap dump 和 thread dump 的采集流程
事故响应
- OOMKilled:先看 cgroup 和 NMT,不要盲目加内存
- Heap OOM:拿 heap dump,分析 dominator tree
- GC 风暴:看 Full GC、Humongous、分配速率
- 线程泄漏:thread dump/JFR
- 回滚最近变更,保留现场
成本
- 过度堆配置浪费内存
- ZGC 的并发线程和内存余量有成本
- 用 rightsizing 和分层 SLO 控制成本
- 不是所有服务都需要 ZGC/大堆
SRE 反模式
- 只监控 Heap,不看 RSS/native
- 用平均延迟掩盖尾延迟
- 复制别人的 JVM 参数
- 没有 GC 日志
- OOM 后直接重启,不保留现场
- 把 GC 调优当一次性任务
- 没有容量模型,靠报警扩容
企业实践
服务分层
- 核心低延迟:ZGC/G1 + 严格 SLO + 持续 JFR
- 通用服务:G1 + 75% 堆 + 监控
- 批处理:Parallel + 吞吐优先
- 边车/工具:Serial/小堆
- 性能测试:Epsilon(隔离 GC)
容器基线
-XX:MaxRAMPercentage=75.0 |
按服务调整,不要一刀切。
JDK 升级
- 25 LTS 作为生产基线
- 27 非 LTS 先评估
- 升级前跑 GC/性能回归
- 关注默认 GC、容器支持、JFR、弃用参数
- 保留旧 JDK 回滚
反模式
- 堆设成容器 limit 的 100%
- 只看 GC 次数不看停顿
- 用
-Xmn硬调 G1 Young - 用
-XX:+DisableExplicitGC掩盖问题 - 不保留 heap dump
- 没有基线就调参
- 所有服务复制同一套参数
附录
常用参数
# 堆 |
常用命令
jcmd <pid> GC.heap_info |