第十四篇:JVM参数调优实战——从GC日志到参数调整
系列文章目录第一篇一段旅程的开始——JVM内存模型简介97第二篇对象的内存布局——从类型指针到OOP-Klass模型96第三篇从一段代码看透JVM内存布局对象、Klass、Method的底层真相95第四篇类加载机制——从.class到Klass的完整旅程96第五篇方法区的进化——永久代到元空间为什么要变96第六篇GC Roots与可达性分析——对象是如何被标记存活的96第七篇引用类型——强、软、弱、虚你还在用强引用吗96第八篇Stop The World——GC为什么会让程序卡顿96第九篇MinorGC完整流程与复制算法深度解析96第十篇动态年龄判定与空间分配担保——MinorGC背后的“潜规则”94第十一篇FullGC深度解析——当老年代也撑不住的时候96第十二篇CMS与G1垃圾回收器深度剖析96第十三篇直接内存与零拷贝——NIO性能优化的底层真相96第十四篇JVM参数调优实战——从GC日志到参数调整-96第十五篇OOM排查实战——从一个内存泄漏案例说起-96前言在前面的文章中我们深入学习了JVM的内存布局、类加载机制、垃圾回收原理以及CMS和G1等回收器的实现细节。但理论知识最终要服务于实战——如何通过调整JVM参数来优化应用性能很多同学在面对JVM调优时常常感到无从下手堆内存设置多大新生代和老年代比例多少GC日志怎么看什么时候该调参数今天我们就来系统学习JVM参数调优的方法论。读完本文你将能回答如何开启和分析GC日志核心JVM参数有哪些如何设置如何根据GC日志定位问题调优的步骤和原则是什么下一篇我们将进入OOM排查实战——从一个内存泄漏案例说起。一、调优前的准备开启GC日志1.1 为什么需要GC日志GC日志是JVM调优的“黑匣子”。没有日志你就像在黑暗中摸索。通过GC日志你可以了解GC的频率和耗时判断是否存在内存泄漏评估GC调优的效果1.2 GC日志参数配置# JDK 8及之前的GC日志参数-XX:PrintGCDetails# 打印GC详细信息-XX:PrintGCDateStamps# 打印GC发生的时间日期格式-XX:PrintGCTimeStamps# 打印GC发生的时间从JVM启动开始-XX:PrintTenuringDistribution# 打印对象年龄分布-XX:PrintHeapAtGC# 打印GC前后的堆信息-Xloggc:/path/to/gc.log# 指定GC日志输出路径-XX:UseGCLogFileRotation# 启用GC日志轮转-XX:NumberOfGCLogFiles10# 保留的GC日志文件数-XX:GCLogFileSize10M# 每个GC日志文件大小# JDK 9的统一日志格式-Xlog:gc*:file/path/to/gc.log:time,uptime,level,tags:filecount10,filesize10M1.3 完整的GC日志配置示例# 生产环境推荐的GC日志配置-XX:PrintGCDetails-XX:PrintGCDateStamps-XX:PrintTenuringDistribution-XX:PrintHeapAtGC-XX:PrintGCApplicationStoppedTime-XX:PrintGCApplicationConcurrentTime-Xloggc:/data/logs/gc.log-XX:UseGCLogFileRotation-XX:NumberOfGCLogFiles10-XX:GCLogFileSize10M二、GC日志解读实战2.1 MinorGC日志解读2024-01-01T10:00:00.1230800: [GC (Allocation Failure) 2024-01-01T10:00:00.1230800: [ParNew: 51200K-5120K(58880K), 0.0123456 secs] 51200K-10240K(189440K), 0.0125678 secs] [Times: user0.03 sys0.00, real0.01 secs]逐段解读字段含义2024-01-01T10:00:00.1230800GC发生的时间GC (Allocation Failure)GC类型MinorGC原因分配失败ParNew新生代回收器Parallel New51200K-5120K(58880K)新生代回收前51200K→回收后5120K总容量58880K51200K-10240K(189440K)堆总回收前51200K→回收后10240K总容量189440K0.0125678 secsGC总耗时Times: user0.03 sys0.00, real0.01CPU耗时用户态0.03s内核态0.00s实际耗时0.01s2.2 FullGC日志解读2024-01-01T10:00:00.1230800: [Full GC (System.gc()) [PSYoungGen: 51200K-0K(58880K)] [ParOldGen: 102400K-51200K(204800K)] 153600K-51200K(263680K), [Metaspace: 10240K-10240K(106496K)], 0.5234567 secs] [Times: user0.52 sys0.00, real0.52 secs]逐段解读字段含义Full GC (System.gc())FullGC触发原因显式调用System.gc()PSYoungGen: 51200K-0K(58880K)新生代回收前51200K→回收后0KParOldGen: 102400K-51200K(204800K)老年代回收前102400K→回收后51200K153600K-51200K(263680K)堆总回收前153600K→回收后51200KMetaspace: 10240K-10240K(106496K)元空间无变化0.5234567 secs耗时0.52秒2.3 年龄分布日志Desired survivor size 5242880 bytes, new threshold 7 (max 15) - age 1: 1048576 bytes, 1048576 total - age 2: 2097152 bytes, 3145728 total - age 3: 4194304 bytes, 7340032 total - age 4: 1048576 bytes, 8388608 total解读Desired survivor size 5242880 bytesSurvivor区期望使用量5MBnew threshold 7本次GC后的晋升阈值为7年龄3的对象总大小4MB加上年龄1-2的3MB总共7MB 5MB所以阈值降为72.4 常见GC日志模式模式1健康的GC[GC(Allocation Failure)[ParNew: 51200K-5120K(58880K),0.012s]51200K-10240K(189440K),0.012s][GC(Allocation Failure)[ParNew: 51200K-5184K(58880K),0.013s]10240K-10800K(189440K),0.013s][GC(Allocation Failure)[ParNew: 51200K-5248K(58880K),0.012s]10800K-11400K(189440K),0.012s]特征GC频率稳定每次回收后内存使用量缓慢增长GC耗时稳定0.01-0.02s模式2频繁GC[GC(Allocation Failure)[ParNew: 51200K-10240K(58880K),0.012s]51200K-10240K(189440K),0.012s][GC(Allocation Failure)[ParNew: 51200K-10240K(58880K),0.013s]10240K-15360K(189440K),0.013s][GC(Allocation Failure)[ParNew: 51200K-10240K(58880K),0.012s]15360K-20480K(189440K),0.012s]特征GC频率高每次回收后内存快速增长可能原因堆内存太小、对象分配过快模式3晋升失败[GC(Allocation Failure)[ParNew: 51200K-51200K(58880K),0.012s][Full GC(Promotion Failed)[ParNew: 51200K-0K(58880K)][ParOldGen: 102400K-51200K(204800K)]...]特征MinorGC后存活对象全部留在Survivor回收前回收后立即触发FullGC原因老年代空间不足或碎片化模式4内存泄漏[Full GC(Ergonomics)[PSYoungGen: 51200K-51200K(58880K)][ParOldGen: 102400K-102400K(204800K)]...][Full GC(Ergonomics)[PSYoungGen: 51200K-51200K(58880K)][ParOldGen: 102400K-102400K(204800K)]...]特征FullGC后内存使用量不变连续多次FullGC都无法回收说明存在内存泄漏或内存确实不够三、核心JVM参数详解3.1 内存相关参数参数说明示例建议-Xms堆初始大小-Xms2g设为与-Xmx相同避免动态扩展-Xmx堆最大大小-Xmx2g根据机器内存和应用需求设置-Xmn新生代大小-Xmn1g通常为堆的1/3-1/4-XX:NewRatio老年代/新生代比例-XX:NewRatio2老年代:新生代2:1-XX:SurvivorRatioEden/Survivor比例-XX:SurvivorRatio8Eden:Survivor8:1:1-XX:MaxMetaspaceSize元空间最大大小-XX:MaxMetaspaceSize256m根据类加载数量设置-XX:MaxDirectMemorySize直接内存大小-XX:MaxDirectMemorySize512m根据NIO使用情况设置3.2 GC相关参数参数说明示例-XX:UseSerialGC串行GC适合单CPU、小内存-XX:UseParallelGC并行GC新生代JDK8默认服务端模式-XX:UseParallelOldGC并行GC老年代配合UseParallelGC使用-XX:UseConcMarkSweepGCCMS低停顿适合4GB以下-XX:UseG1GCG1可预测停顿适合4GB以上-XX:ParallelGCThreadsGC线程数-XX:ParallelGCThreads8-XX:ConcGCThreads并发GC线程数-XX:ConcGCThreads43.3 GC行为参数参数说明示例-XX:MaxTenuringThreshold最大晋升阈值-XX:MaxTenuringThreshold15-XX:TargetSurvivorRatioSurvivor目标使用率-XX:TargetSurvivorRatio50-XX:PretenureSizeThreshold大对象直接进入老年代的阈值-XX:PretenureSizeThreshold2m-XX:DisableExplicitGC禁用System.gc()生产环境建议开启-XX:ExplicitGCInvokesConcurrentSystem.gc()触发并发GCCMS/G1下使用3.4 调试和日志参数参数说明-XX:PrintGCDetails打印GC详细信息-XX:PrintGCDateStamps打印GC时间戳日期格式-XX:PrintTenuringDistribution打印对象年龄分布-Xloggc:/path/to/gc.logGC日志输出路径-XX:HeapDumpOnOutOfMemoryErrorOOM时自动导出堆转储-XX:HeapDumpPath/path/to/dump堆转储文件路径四、调优步骤和方法论4.1 调优流程┌─────────────────────────────────────────────────────────────────────┐ │ JVM调优流程 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. 确定目标 │ │ ├─ 吞吐量优先还是延迟优先 │ │ ├─ 期望的GC停顿时间是多少 │ │ └─ 可接受的GC频率是多少 │ │ ↓ │ │ 2. 收集基线数据 │ │ ├─ 开启GC日志 │ │ ├─ 运行压测收集GC数据 │ │ └─ 分析当前GC频率、耗时、内存使用 │ │ ↓ │ │ 3. 分析问题 │ │ ├─ 是GC太频繁还是GC耗时太长 │ │ ├─ 是新生代问题还是老年代问题 │ │ └─ 是否存在内存泄漏 │ │ ↓ │ │ 4. 调整参数 │ │ ├─ 一次只调整一个参数 │ │ ├─ 记录调整前后的对比数据 │ │ └─ 小步快跑逐步优化 │ │ ↓ │ │ 5. 验证效果 │ │ ├─ 重新压测收集数据 │ │ ├─ 对比优化前后的指标 │ │ └─ 如果未达目标回到步骤3 │ │ │ └─────────────────────────────────────────────────────────────────────┘4.2 常见问题及解决方案问题可能原因解决方案MinorGC频繁新生代太小增大-Xmn或调整NewRatioMinorGC耗时过长Survivor区太小对象直接晋升增大SurvivorRatio让对象在Survivor区多停留FullGC频繁老年代增长快内存泄漏或对象过早晋升检查内存泄漏增大晋升阈值FullGC频繁老年代使用率高老年代空间不足增大-Xmx或优化缓存FullGC耗时过长老年代碎片化或对象太多使用CMS或G1开启碎片整理CMS并发模式失败对象分配速度超过CMS回收速度降低CMS触发阈值增大老年代G1 Mixed GC频繁IHOP阈值太低调整InitiatingHeapOccupancyPercentSTW时间过长GC线程数不足或安全点问题增加GC线程数优化代码中的长循环4.3 调优案例1MinorGC频繁现象[GC(Allocation Failure)[ParNew: 25600K-512K(30720K),0.012s]...][GC(Allocation Failure)[ParNew: 25600K-512K(30720K),0.013s]...][GC(Allocation Failure)[ParNew: 25600K-512K(30720K),0.012s]...]# GC间隔只有1-2秒分析新生代只有30MB每次回收后使用量512K但很快又涨到25MB说明对象分配快新生代太小。解决方案# 原配置-Xms2g-Xmx2g-Xmn256m-XX:SurvivorRatio8# 优化增大新生代到512MB-Xms2g-Xmx2g-Xmn512m-XX:SurvivorRatio84.4 调优案例2晋升失败频繁触发FullGC现象[GC(Allocation Failure)[ParNew: 51200K-51200K(58880K),0.012s][Full GC(Promotion Failed)...][GC(Allocation Failure)[ParNew: 51200K-51200K(58880K),0.013s][Full GC(Promotion Failed)...]分析每次MinorGC后存活对象占满Survivor区导致对象直接晋升到老年代。老年代空间不足触发FullGC。解决方案# 原配置-Xms2g-Xmx2g-Xmn512m-XX:SurvivorRatio8# 优化1增大Survivor区比例-Xms2g-Xmx2g-Xmn512m-XX:SurvivorRatio6# Eden:Survivor6:1:1# 优化2增大晋升阈值-XX:MaxTenuringThreshold15# 优化3调整动态年龄判定阈值-XX:TargetSurvivorRatio70# 默认50%4.5 调优案例3CMS并发模式失败现象[GC(Allocation Failure)[ParNew: 51200K-5120K(58880K),0.012s][CMS(concurrent mode failure): 153600K-102400K(204800K),0.523s]分析CMS并发回收速度跟不上对象分配速度老年代在并发标记期间被填满。解决方案# 原配置-XX:UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction92# 优化1降低CMS触发阈值更早开始回收-XX:CMSInitiatingOccupancyFraction75# 优化2增加CMS线程数-XX:ConcGCThreads4# 优化3考虑切换到G1-XX:UseG1GC五、常用调优工具5.1 jstat实时监控GC# 查看GC情况每1秒输出一次jstat-gcutil123451000S0 S1 E O M CCS YGC YGCT FGC FGCT GCT0.0098.2345.1245.6792.3491.231251.23452.5673.801字段解读S0/S1Survivor区使用率EEden区使用率O老年代使用率M元空间使用率YGC/YGCTYoung GC次数和总耗时FGC/FGCTFull GC次数和总耗时5.2 jmap堆转储# 导出堆转储会触发FullGCjmap -dump:live,formatb,fileheap.hprof12345# 查看堆内存统计jmap-histo12345|head-20# 查看堆内存统计只统计存活对象jmap-histo:live12345|head-205.3 jcmd综合诊断# 查看JVM进程信息jcmd12345VM.flags# 查看系统属性jcmd12345VM.system_properties# 手动触发GCjcmd12345GC.run# 查看GC日志配置jcmd12345GC.heap_info5.4 图形化工具工具用途获取方式VisualVM监控、堆转储分析JDK自带JConsole基础监控JDK自带MAT堆转储分析eclipse.org/matGCViewerGC日志分析github.com/chewiebug/GCViewerGCEasy在线GC日志分析gceasy.io六、调优参数速查表6.1 内存参数# 堆内存-Xms2g-Xmx2g# 堆大小2GB-Xmn512m# 新生代512MB-XX:NewRatio2# 老年代:新生代2:1-XX:SurvivorRatio8# Eden:Survivor8:1:1# 元空间-XX:MetaspaceSize256m# 元空间初始大小-XX:MaxMetaspaceSize256m# 元空间最大大小# 直接内存-XX:MaxDirectMemorySize512m# 直接内存大小6.2 GC参数# 串行GC单CPU、小内存-XX:UseSerialGC# 并行GC吞吐量优先-XX:UseParallelGC-XX:UseParallelOldGC-XX:ParallelGCThreads8# CMS低停顿4GB以下-XX:UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction75-XX:UseCMSCompactAtFullCollection-XX:ConcGCThreads4# G1可预测停顿4GB以上-XX:UseG1GC-XX:MaxGCPauseMillis200-XX:G1HeapRegionSize16m-XX:InitiatingHeapOccupancyPercent456.3 调试参数# GC日志-XX:PrintGCDetails-XX:PrintGCDateStamps-XX:PrintTenuringDistribution-Xloggc:/data/logs/gc.log# OOM处理-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/data/dumps-XX:ExitOnOutOfMemoryError# 显式GC-XX:DisableExplicitGC七、常见面试题Q1如何确定堆内存大小答堆内存大小需要根据应用的内存占用和GC情况确定。一般原则初始堆-Xms和最大堆-Xmx设为相同值避免动态扩展堆大小应能满足业务高峰期的内存需求堆大小不应超过物理内存的60%-70%为操作系统和其他进程留出空间通过压测找到GC频率和耗时可接受的最大堆大小Q2新生代和老年代的比例如何设置答默认比例-XX:NewRatio2新生代占1/3适合大多数应用如果应用创建大量临时对象可增大新生代比例如果应用缓存较多对象存活时间长可减小新生代比例一般建议新生代占堆的1/3到1/4Q3什么时候应该启用GC日志答生产环境始终应该开启GC日志。GC日志开销很小但能为问题排查提供关键信息。建议配置日志轮转避免日志文件过大。Q4如何判断是否需要调优答如果GC满足以下条件通常不需要调优MinorGC间隔5秒耗时50msFullGC间隔1小时耗时1秒老年代使用率稳定在50%-80%如果出现以下情况需要调优GC频率过高MinorGC1秒FullGC1小时GC耗时过长MinorGC100msFullGC2秒老年代使用率持续90%Q5调优时应该先调哪个参数答建议按以下顺序调整先确定合理的堆大小-Xms、-Xmx再调整分代比例-Xmn、-XX:NewRatio然后调整Survivor区比例-XX:SurvivorRatio最后调整GC回收器相关参数每次只调整一个参数观察效果八、总结8.1 调优核心要点要点说明开启GC日志调优的基础没有日志就是盲人摸象明确目标延迟优先还是吞吐优先一次只改一个参数便于评估效果压测验证生产环境调优前必须压测持续监控调优不是一次性的需要持续关注8.2 调优参数速记堆大小-Xms和-Xmx相同 新生代1/3到1/4的堆 SurvivorEden的1/8到1/6 元空间根据类加载数量 直接内存根据NIO使用 GC回收器小堆CMS大堆G1 GC日志生产环境必开8.3 面试金句如果面试官问你“JVM调优的步骤和方法”你可以这样回答“JVM调优的第一步是开启GC日志没有日志就无法定位问题。然后通过压测收集基线数据分析GC频率和耗时。调优时一次只调整一个参数逐步优化。核心参数包括堆大小-Xms、-Xmx、新生代大小-Xmn、Survivor比例-XX:SurvivorRatio、元空间大小-XX:MaxMetaspaceSize。GC回收器的选择4GB以下用CMS4GB以上用G1。调优的目标是在可接受的GC停顿时间内最大化应用吞吐量。调优完成后还需要持续监控确保GC行为稳定。”下篇预告掌握了JVM参数调优我们已经可以主动优化应用性能。但有些问题不是调参能解决的——比如内存泄漏。下一篇《OOM排查实战——从一个内存泄漏案例说起》将带你从头到尾排查一个真实的内存泄漏案例学习使用MAT、VisualVM等工具定位问题。如果你觉得本文有帮助欢迎点赞、评论、转发