即使代码结构清晰、框架使用得当,当系统压力上升到一定程度,性能瓶颈最终往往落在 JVM 本身——堆内存分配不合理、GC 停顿时间过长、内存泄漏等问题会直接拖垮服务。掌握基本的 JVM 内存模型和 GC 调优方法,是 Java 开发者从“会用”迈向“会用得好”的关键一步。
21.5.1 内存区域与关键参数
JVM 内存可粗略分为堆内存和非堆内存。其中堆内存是 GC 的主要管理区域,也是调优的重心。现代 HotSpot VM 的堆通常分为三个代:
- 年轻代(Young Generation):绝大多数新对象在此分配,由 Eden 区和两个 Survivor 区(S0、S1)组成。年轻代回收称为 Minor GC,频率高、耗时相对较短。
- 老年代(Old Generation / Tenured):存活时间长、经过多次 Minor GC 依然存活的对象会晋升到这里。老年代回收称为 Major GC 或 Full GC,通常伴随较长的 STW(Stop-The-World)停顿。
- 元空间(Metaspace,JDK 8 起代替永久代):存放类的元数据,不在堆内,但若未限制其大小,在动态生成大量类的场景下可能导致 OOM。
常用的堆内存参数如下:
| 参数 | 含义 | 示例 |
|------|------|------|
| -Xms | 初始堆大小 | -Xms2g |
| -Xmx | 最大堆大小 | -Xmx4g |
| -Xmn 或 -XX:NewSize / -XX:MaxNewSize | 年轻代大小 | -Xmn1g |
| -XX:MetaspaceSize / -XX:MaxMetaspaceSize | 元空间初始/最大大小 | -XX:MaxMetaspaceSize=256m |
| -XX:SurvivorRatio | Eden 与单个 Survivor 的比例,默认 8 | -XX:SurvivorRatio=8 |
| -XX:MaxTenuringThreshold | 对象晋升老年代前经历的 GC 次数,默认 15 | -XX:MaxTenuringThreshold=15 |
实用建议:
- 生产环境务必设置
-Xms和-Xmx相等,避免堆扩缩容带来的性能抖动。 - 年轻代大小一般设为整个堆的 1/4 ~ 1/2,可通过 GC 日志观察 Minor GC 频率和晋升情况来调节。
- 对于大部分微服务应用,堆大小 2~4 GB 是一个常见的起点,再大则需要考虑 GC 停顿耗时问题。
21.5.2 垃圾收集器的选择与特点
JDK 8 至 JDK 21 提供了多种 GC 实现,不同场景适用不同的收集器。从“明了实用”的角度,记住几个主流选项即可:
1. 串行 GC(Serial GC)
- 启用参数:
-XX:+UseSerialGC - 特点:单线程回收,会触发较长的 STW。
- 适用:客户端应用或内存极小的场景(几百 MB),现代服务端基本不推荐。
2. 并行 GC(Parallel GC)
- 启用参数:
-XX:+UseParallelGC - 特点:多线程并行回收,追求高吞吐量,GC 时仍会 STW。JDK 8 服务器端的默认 GC。
- 适用:批处理、后台计算任务,对停顿不敏感的场景。
3. 并发标记清除 GC(CMS GC)
- 启用参数:
-XX:+UseConcMarkSweepGC - 特点:老年代采用并发标记和清除,大部分工作与用户线程并发执行,追求低停顿。但会产生内存碎片,当无法分配连续空间时退化为串行 Full GC。JDK 14 已被移除,存量系统应尽快迁移。
- 适用:仅老系统维护时需要了解。
4. Garbage-First GC(G1 GC)
- 启用参数:
-XX:+UseG1GC(JDK 9 起为默认 GC) - 特点:将堆划分为多个 Region,优先回收垃圾最多的区域,支持可预期的停顿时间目标(
-XX:MaxGCPauseMillis)。兼顾吞吐量和低延迟。 - 适用:大多数服务端应用,堆在 4 GB 以上时表现优异,已成为主流选择。
5. ZGC 与 Shenandoah
- 启用参数:
-XX:+UseZGC(JDK 11 引入,JDK 15 生产就绪)或-XX:+UseShenandoahGC - 特点:低延迟 GC,目标是将停顿时间控制在毫秒级(亚毫秒至数毫秒),即使堆达到 TB 级别也几乎不增加停顿。ZGC 采用染色指针技术,Shenandoah 使用 Brooks Pointer 进行并发整理。
- 适用:对延迟极度敏感的服务,如高频交易、实时交互系统。其中 ZGC 是 JDK 未来长期演进的低延迟方案,在 JDK 17/21 中已非常稳定。
选择建议:对于绝大多数 Spring Boot 微服务,直接使用 G1 GC 即可。如果服务要求 99.9% 的请求响应时间在几十毫秒内,且堆用量较高,可以考虑升级到 JDK 17+ 并启用 ZGC。
21.5.3 GC 调优的实战思路
GC 调优不是盲目的参数调整,而是一个观测 → 分析 → 调整 → 验证的闭环过程。
1. 建立基线,打开 GC 日志
每次启动应用都应加上 GC 日志参数,这是观察 GC 行为的基础设施:
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=10,filesize=100M
(如果还在 JDK 8 环境,格式为:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log)
日志中可以提取的关键指标:Minor GC 频率、单次耗时、对象晋升量、Full GC 次数与耗时、GC 暂停总时间占比。
2. 明确调优目标
GC 调优的终极目标不是“完全没有 GC”,而是让 GC 的停顿符合业务 SLA。常见目标方向:
- 降低停顿时间:减少单次暂停的时长,适用于在线服务。可以尝试 G1/ZGC,并调整
-XX:MaxGCPauseMillis(例如设为 200)。 - 提升吞吐量:最大化 CPU 用于业务的比例,减少 GC 的总 CPU 开销。适用于后台任务。可以使用 Parallel GC,或适当增大年轻代减少 Minor GC 频次。
- 解决内存问题:如频繁 Full GC、内存持续上涨。需分析内存泄漏或对象晋升异常。
3. 常见问题与调整策略
| 现象 | 可能原因 | 调整方向 |
|------|----------|----------|
| Minor GC 频繁,但每次回收量小 | 年轻代过小,对象频繁创建并迅速消亡 | 增大年轻代(-Xmn)或调整 Eden/Survivor 比例 |
| Minor GC 后存活对象过多,大量晋升老年代 | Survivor 空间不足,或对象生命周期比预想长 | 加大 Survivor 区、提高 MaxTenuringThreshold |
| 老年代增长缓慢但持续,最终 Full GC 频繁 | 存在缓慢的内存泄漏,或对象晋升速度高于老年代回收速度 | 排查泄漏(dump 堆分析),增大老年代(即增大 -Xmx) |
| CMS / G1 并发失败,退化为长时间 Full GC | 并发回收速度跟不上分配速度,老年代或堆过小 | 增大堆、降低并行回收阈值、优化代码减少对象生成 |
| 元空间 OutOfMemoryError | 动态类加载(如 CGLib、反射、动态语言)过多 | 设置 -XX:MaxMetaspaceSize 限制并排查类加载器泄漏 |
4. 工具辅助分析
- 命令行:
jstat -gc <pid> 1s实时查看各代内存占用与 GC 统计。 - 在线分析:Arthas 的
dashboard、vmtool可以动态监测内存与线程。 - 离线堆 dump:
jmap -dump:live,format=b,file=heap.hprof <pid>,然后使用 Eclipse Memory Analyzer (MAT) 或 IDEA 内置 Profiler 分析大对象、引用链路。 - GC 日志可视化:将 gc.log 上传到 GCEasy (gceasy.io) 或 GCViewer 生成图表,直观看到停顿时间分布、吞吐量趋势。
21.5.4 一个典型的 G1 调优配置示例
假设一个 Spring Boot 微服务,分配了 4 GB 物理内存,期望 99% 的请求在 200 ms 内完成。JVM 参数参考配置:
-Xms3g -Xmx3g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:ParallelGCThreads=4 \
-XX:ConcGCThreads=2 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:G1ReservePercent=5 \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100M \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/heapdump.hprof
解释:
- 堆 3 GB,预留 1 GB 给系统、元空间、线程栈等。
MaxGCPauseMillis=200告诉 G1 尽量将单次暂停控制在 200 ms 内,这是一个软目标,并非硬保证。InitiatingHeapOccupancyPercent=45表示当堆占用达到 45% 时启动并发标记周期,给回收留出足够缓冲,避免并发失败。G1ReservePercent=5保留一定比例空闲空间以应对“晋升失败”。- 开启 OOM 时自动 dump 堆,便于事后排查。
此配置可以在压测环境中验证:观察 GC 日志中 Pause Young、Pause Mixed 的耗时是否稳定在 200 ms 以下,Full GC 是否消失,吞吐量是否满足要求。根据实际情况微调堆大小与 Young/Initating 参数。
21.5.5 内存与 GC 调优的底线原则
- 不要过早优化:先让应用跑起来,有了性能数据和 GC 日志,再对症下药。
- 向上汇报靠指标,向下排查靠 dump:用 Prometheus + Grafana 监控堆使用量、GC 次数和耗时,趋势异常时保留现场 dump 文件进行分析。
- JVM 参数变更须逐步进行:一次只改一两个参数,观察效果,避免参数调整引入的新问题掩盖旧问题。
- 优先从应用代码层面解决内存问题:大多数 GC 问题的根源是代码生成了过多临时对象或存在泄漏,调优 JVM 只是治标,修复代码才是治本。
掌握这些知识和实践套路,足够应对 90% 以上的 JVM 内存与 GC 相关性能问题。在下一节中,我们将聚焦于应用层面的常见性能隐患排查与优化策略。