JVM - 调优概述
# 1. 为何需要 JVM 调优?—— 来自大厂的拷问
在探讨技术细节之前,我们先来看看一线大厂对于 JVM 调优的关注点,这有助于我们理解其重要性:
- 支付宝:JVM 性能调优具体做了哪些工作?(考察实践经验和调优深度)
- 小米:是否进行过 JVM 内存优化?从 SQL、JVM、架构、数据库四个维度谈谈优化思路。(考察系统性优化思维)
- 蚂蚁金服:JVM 的编译优化了解吗?性能调优实践?用过哪些诊断工具?堆栈空间如何设置?性能调优步骤?(考察 JVM 底层、工具使用和调优流程)
- 阿里:如何进行 JVM 调优,有哪些方法?内存泄漏如何理解、排查和解决?(考察调优方法论和问题解决能力)
- 字节跳动:JVM 如何调优,参数如何调整?(考察参数理解和实践能力)
- 拼多多:从 SQL、JVM、架构、数据库四个方面谈优化思路。(再次强调系统性思维)
- 京东:诊断工具使用?高并发秒杀系统 GC 问题?百万级交易系统 JVM 优化?线上 OOM 监控与解决?G1 垃圾回收器优化?(考察高并发、复杂场景下的调优能力)
这些问题覆盖了从基础概念到复杂场景实战的方方面面,充分说明了 JVM 调优是 Java 工程师必备的核心技能。
# 2. JVM 调优的背景与动机
# 2.1 生产环境中的常见“痛点”
在实际的生产环境中,我们可能会遇到各种与 JVM 相关的问题:
- 内存溢出(OOM):这是最严重的问题之一,直接导致应用崩溃。我们需要知道如何快速处理和定位 OOM 的根源。
- 内存配置不当:服务器内存分配过多导致资源浪费,分配过少则容易引发 OOM 或频繁 GC。合理的内存配置至关重要。
- GC 性能不佳:垃圾回收过于频繁或单次回收时间过长(尤其是 Full GC),会导致应用卡顿(STW, Stop-The-World),影响用户体验。
- CPU 负载过高:有时并非业务逻辑复杂,而是 JVM 内部活动(如 GC、线程竞争)导致 CPU 飙升。
- 线程数不合理:线程池配置过大或过小都可能影响系统性能和稳定性。
- 代码执行路径未知:在不方便添加日志的情况下,如何确定请求是否执行了某段关键代码?
- 方法调用追踪困难:如何在不修改代码和重启服务的情况下,实时查看方法的输入参数和返回值?
# 2.2 调优的核心目标
进行 JVM 调优,主要目的是:
- 预防 OOM:通过合理的内存配置和代码规范,从源头上减少 OOM 的可能性。
- 解决 OOM:当 OOM 发生时,能够快速定位问题原因(内存泄漏、内存不足等)并采取有效措施。
- 减少 Full GC 频率:Full GC 对应用性能影响最大,调优的目标之一就是尽量避免或减少 Full GC 的发生,优化 GC 停顿时间。
# 2.3 不同阶段的调优策略
JVM 调优并非一蹴而就,需要在不同阶段采取不同的策略:
- 上线前:
- 基准测试:评估当前配置下的性能表现。
- 容量规划:根据预期的业务量和压力测试结果,合理规划 JVM 内存、线程数等资源。
- 选择合适的 GC 策略:根据应用的特性(响应时间敏感型 vs. 吞吐量优先型)选择合适的垃圾收集器。
- 项目运行阶段:
- 持续监控:建立完善的监控体系,实时关注 JVM 的各项性能指标(堆内存、GC、线程、CPU 等)。
- 日志分析:定期分析 GC 日志,观察 GC 模式和性能变化。
- 预警机制:设置关键指标的阈值告警,提前发现潜在风险。
- 线上出现 OOM 或性能问题:
- 应急处理:快速恢复服务是首要任务(如重启、扩容)。
- 问题定位:保存现场信息(堆转储、线程快照、GC 日志),进行深入分析。
- 根源解决:根据分析结果,调整 JVM 参数、优化代码或改进架构。
# 3. JVM 调优的宏观思路
# 3.1 性能问题的“侦探工具”—— 监控依据
定位和解决 JVM 问题,离不开以下关键信息来源:
- 运行日志:应用程序输出的业务日志和异常信息,是理解业务逻辑执行情况的基础。
- 异常堆栈 (Exception Stack Trace):当程序抛出异常时,堆栈信息能精确显示异常发生的位置和调用链,对于定位错误至关重要。
- GC 日志 (Garbage Collection Log):记录了 GC 事件的详细信息,包括 GC 类型、发生时间、持续时间、回收了多少内存、GC 前后堆内存的使用情况等。是分析 GC 性能和内存问题的核心依据。
- 开启方式示例:
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
- 开启方式示例:
- 线程快照 (Thread Dump / Jstack):反映了某一时刻 JVM 内所有线程的状态,包括线程的运行状态(Running, Waiting, Blocked 等)、锁信息、堆栈调用信息。主要用于分析线程死锁、线程阻塞、CPU 占用过高等问题。
- 获取方式:
jstack <pid>
- 获取方式:
- 堆转储快照 (Heap Dump):记录了某一时刻 Java 堆内存中所有对象的详细信息,包括对象类型、数量、大小、引用关系等。是分析内存泄漏、大对象、内存占用的权威依据。
- 获取方式:
jmap -dump:format=b,file=heap.hprof <pid>
或 OOM 时自动转储 (-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
)
- 获取方式:
# 3.2 调优的三大方向
JVM 性能优化是一个系统工程,可以从以下三个主要方向入手:
- 合理地编写代码 (Code Optimization):
- 优化数据结构和算法:选择合适的集合类,优化循环和递归。
- 减少对象创建和内存占用:使用
StringBuilder
代替+
拼接字符串,利用对象池技术,避免创建过多临时对象,谨慎使用大对象。 - 合理使用锁:减少锁的粒度,使用并发包提供的更高效的锁机制(如
ReentrantLock
,StampedLock
)。 - 及时释放资源:确保 IO 流、数据库连接等资源在使用完毕后被正确关闭。
- 充分并合理的使用硬件资源 (Hardware Resource Utilization):
- CPU:了解应用是 CPU 密集型还是 IO 密集型,合理配置线程数,避免线程过多导致上下文切换开销。
- 内存:根据应用需求和服务器物理内存,合理设置 JVM 堆大小(
-Xms
,-Xmx
)、新生代与老年代比例(-XX:NewRatio
)、Metaspace 大小(-XX:MetaspaceSize
,-XX:MaxMetaspaceSize
)。 - 磁盘 I/O:优化磁盘读写操作,考虑使用缓存或异步处理。
- 网络 I/O:优化网络通信协议和数据传输方式。
- 合理地进行 JVM 调优 (JVM Tuning):
- 内存调优:调整堆各区域的大小,优化对象分配和晋升。
- GC 调优:选择合适的垃圾收集器(Serial, Parallel, CMS, G1, ZGC, Shenandoah),调整 GC 相关参数(如
-XX:MaxGCPauseMillis
,-XX:ParallelGCThreads
)以平衡吞吐量和停顿时间。 - 线程调优:调整 JVM 线程栈大小(
-Xss
)。
# 4. 性能优化的标准步骤:监控 -> 分析 -> 调优
JVM 性能优化通常遵循以下三个步骤:
# 第 1 步:性能监控 (Performance Monitoring) —— 发现问题的“眼睛”
定义:以非侵入或低侵入的方式,收集和查看 应用运行时的性能数据。这是一种 预防性或主动性 的活动,通常在生产、测试或开发环境中进行。
目的:持续观察应用的健康状况,及时发现性能瓶颈或异常迹象。
监控什么?
- GC 状态:GC 的频率、类型(Minor GC / Major GC / Full GC)、单次耗时、总耗时占比、GC 前后内存变化。频繁或耗时过长的 GC 是重点关注对象。
- JVM 内存:堆内存(Heap)各区域(Eden, Survivor, Old Gen)的使用率、非堆内存(Metaspace/PermGen)使用率、内存池的使用情况。持续增长的内存使用可能预示着内存泄漏。
- CPU 负载:JVM 进程的 CPU 使用率。持续高 CPU 占用需要分析是业务逻辑导致还是 GC 或线程问题。
- 线程:活动线程数、峰值线程数、线程状态(Runnable, Waiting, Blocked)、是否存在死锁。
- 类加载:已加载类的数量、卸载类的数量。
- 程序响应时间:关键业务接口的平均响应时间、P95/P99 响应时间。
常用监控工具:
- JDK 自带工具:
jstat
: 命令行工具,实时显示类加载、内存、GC、JIT 编译等信息。jconsole
: JDK 自带的图形化监控工具,提供内存、线程、类、CPU 的基本监控。jvisualvm
: 功能更强大的图形化监控和分析工具,支持 Profiling、线程 Dump、堆 Dump。
- 第三方工具/平台:
- Arthas (Alibaba):Java 诊断利器,可以在不重启服务的情况下,动态跟踪代码执行、观测方法出入参、诊断 CPU 热点、查看类加载信息等。
- Prometheus + Grafana: 开源监控告警解决方案,通过 JMX Exporter 或其他 Agent 采集 JVM 指标,进行可视化展示和告警。
- SkyWalking, Pinpoint: 开源 APM (Application Performance Management) 系统,提供分布式追踪、应用拓扑、性能指标监控等全方位能力。
- 商业 APM 工具: 如 Dynatrace, New Relic 等。
# 第 2 步:性能分析 (Performance Analysis) —— 排查问题的“放大镜”
定义:通常在性能监控发现问题后进行,以 可能带有侵入性 的方式收集更详细的运行数据,深入分析性能瓶颈或异常根源。性能分析一般在测试或开发环境中进行,避免影响生产环境。
目的:精准定位导致性能问题的具体原因。
分析方法与工具:
- GC 日志分析:
- 开启 GC 日志:通过 JVM 参数开启,如
-Xloggc:gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
。 - 解读日志:分析 GC 类型、频率、停顿时间、内存回收量、各代内存变化等。
- 可视化工具:使用 GCViewer、GCeasy (
http://gceasy.io
) 等工具导入 GC 日志文件,可以直观地看到 GC 活动图表和分析报告。
- 开启 GC 日志:通过 JVM 参数开启,如
- 命令行工具深入分析:
jstack <pid>
: 获取线程快照,分析死锁(Deadlock)、等待资源(Waiting on condition)、阻塞(Blocked on monitor entry)、消耗 CPU 的线程(Runnable 状态且占用 CPU 高)。多次执行jstack
对比线程状态变化。jmap <pid>
:jmap -heap <pid>
: 查看堆内存概况,包括 GC 算法、堆配置参数、各区域使用情况。jmap -histo <pid>
: 查看堆中对象的统计信息,按对象大小或数量排序,用于查找大对象或过多的小对象。jmap -histo:live <pid>
只看存活对象。jmap -dump:format=b,file=heap.hprof <pid>
: 生成堆转储快照文件。
jinfo <pid>
: 查看和修改 JVM 运行参数。
- 堆转储文件 (Heap Dump) 分析:
- 获取方式:
jmap -dump
命令,或者配置 OOM 时自动 Dump (-XX:+HeapDumpOnOutOfMemoryError
),也可以通过 JConsole, VisualVM, Arthas 获取。 - 分析工具:
- MAT (Memory Analyzer Tool):Eclipse 出品的强大的内存分析工具,可以分析内存泄漏(Leak Suspects Report)、查找大对象(Dominator Tree)、分析对象引用关系等。
- JProfiler: 商业的性能分析工具,功能全面,包含内存分析。
- VisualVM: 也提供了基础的 Heap Dump 分析功能。
- 分析重点:查找内存泄漏点(无法被 GC 回收但不再使用的对象),定位占用内存过多的对象类型和实例。
- 获取方式:
- 实时诊断与观测 (Arthas):
dashboard
: 查看当前系统的实时数据面板。thread
: 查看 JVM 线程状态,特别是thread -b
查找阻塞线程,thread -n <N>
查看最忙的 N 个线程。jvm
: 查看 JVM 信息。heapdump
: dump 堆内存。trace <class> <method>
: 跟踪方法内部调用路径和耗时。watch <class> <method> "{params, returnObj}" -x 2
: 观测方法的入参和返回值。stack <class> <method>
: 输出当前方法被调用的调用路径。sc
/sm
: 查看类/方法信息。redefine
: 热更新代码。
# 第 3 步:性能调优 (Performance Tuning) —— 解决问题的“手术刀”
定义:在性能分析定位到问题后,通过修改 JVM 参数、优化代码、调整配置或架构来改善应用性能(响应性或吞吐量)的活动。
目的:解决性能瓶颈,提升应用表现。
调优手段:
- JVM 参数调优:
- 内存相关:
-Xms<size>
/-Xmx<size>
: 设置堆的初始和最大大小。通常建议设置为相等,避免运行时动态调整堆大小带来的开销和暂停。大小根据可用物理内存和应用需求设定,一般不超过物理内存的 70%-80%。-Xmn<size>
或-XX:NewRatio=<ratio>
/-XX:SurvivorRatio=<ratio>
: 设置年轻代的大小或比例。年轻代大小影响 Minor GC 的频率和耗时。Survivor 区大小影响对象在年轻代的存活时间和进入老年代的阈值。-XX:MetaspaceSize=<size>
/-XX:MaxMetaspaceSize=<size>
: 设置元空间(替代了永久代)的初始和最大大小。如果应用加载大量类,可能需要调大。
- GC 相关:
- 选择收集器:
-XX:+UseSerialGC
: 单线程收集器,适用于客户端或内存较小的应用。-XX:+UseParallelGC
: 并行收集器(吞吐量优先),默认 JDK8 选择,适用于后台计算、对停顿不敏感的应用。-XX:+UseConcMarkSweepGC
(CMS): 并发收集器(低停顿优先),适用于对响应时间要求高的应用(JDK9 后废弃)。-XX:+UseG1GC
: 面向服务端的收集器,平衡了吞吐量和停顿时间,是 JDK9 及以后的默认收集器,适用于大内存应用。-XX:+UseZGC
/-XX:+UseShenandoahGC
: 低延迟收集器,追求极短的停顿时间(亚毫秒级),适用于超大内存和对延迟极其敏感的应用(需要较新 JDK 版本支持)。
- GC 目标参数:
-XX:MaxGCPauseMillis=<ms>
: 设置期望的最大 GC 停顿时间(对 G1, CMS, ZGC, Shenandoah 有效)。JVM 会尽力达成,但这只是一个目标,不保证一定能达到。-XX:GCTimeRatio=<n>
: 设置吞吐量目标,即用户代码执行时间占总时间的比例为n / (n + 1)
。
- GC 线程数:
-XX:ParallelGCThreads=<n>
: 设置并行 GC 的线程数,通常设置为 CPU 核数。-XX:ConcGCThreads=<n>
: 设置并发 GC 的线程数。
- 选择收集器:
- 其他参数:
-Xss<size>
: 设置线程栈大小。如果出现StackOverflowError
且不是由无限递归导致,可以适当调大。-XX:+HeapDumpOnOutOfMemoryError
/-XX:HeapDumpPath=<path>
: OOM 时自动生成 Heap Dump 文件。-XX:+PrintGCDetails
,-XX:+PrintGCDateStamps
,-Xloggc:<file>
: 输出详细 GC 日志。
- 内存相关:
- 代码层面优化:
- 根据性能分析结果,修改存在性能问题的代码逻辑。
- 例如:优化耗时长的 SQL 语句、减少 RPC 调用次数、使用缓存减少计算、优化算法复杂度、修复内存泄漏等。
- 架构与资源调整:
- 增加机器/节点:通过水平扩展分散单节点压力。
- 合理设置线程池大小:根据任务类型(CPU 密集型 vs. IO 密集型)和系统资源调整核心线程数和最大线程数。
- 引入中间件:
- 缓存 (Redis, Memcached): 缓存热点数据,减少对数据库或下游服务的访问。
- 消息队列 (Kafka, RabbitMQ): 实现异步处理、削峰填谷、解耦。
- 数据库优化:索引优化、SQL 调优、分库分表。
- 升级硬件:如果资源确实不足,考虑增加内存、更换 SSD、升级 CPU。
# 5. 衡量优化的标尺 —— 性能评价指标
如何判断调优是否有效?我们需要关注以下关键性能指标:
停顿时间 (Pause Time) / 响应时间 (Response Time):
定义:指应用程序在执行某个操作(如处理请求、执行 GC)时,用户线程被暂停的时间。对于 GC 而言,就是 STW (Stop-The-World) 的时间。对于用户请求,就是从发送请求到收到响应的时间。
关注点:平均响应时间、P95/P99 响应时间(95% 或 99% 的请求都能在此时间内完成)、最大 GC 停顿时间。
调优目标:通常希望停顿时间尽可能短,尤其是对于交互式应用。可以通过
-XX:MaxGCPauseMillis
设置目标。常见操作的响应时间参考:
操作 响应时间 打开一个站点 几秒 数据库查询一条记录(有索引) 十几毫秒 机械磁盘一次寻址定位 4 毫秒 从机械磁盘顺序读取 1M 数据 2 毫秒 从 SSD 磁盘顺序读取 1M 数据 0.3 毫秒 从远程分布式缓存 Redis 读取一个数据 0.5 毫秒 从内存读取 1M 数据 十几微秒 Java 程序本地方法调用 几微秒 网络传输 2Kb 数据 1 微秒
吞吐量 (Throughput):
- 定义:单位时间内系统处理的事务数量或工作量(如 QPS - 每秒查询数, TPS - 每秒事务数)。
- 在 GC 中的定义:用户代码执行时间占总运行时间(用户代码执行时间 + GC 时间)的比例。高吞吐量意味着应用能高效利用 CPU 进行业务处理。
- 计算公式:吞吐量 = 用户代码执行时间 / (用户代码执行时间 + GC 时间)。
- 相关参数:
-XX:GCTimeRatio=n
,表示希望 GC 时间占比不超过1 / (n + 1)
。例如n=19
表示希望 GC 时间占比不超过 5%。 - 调优目标:对于后台计算、批处理等任务,通常追求高吞吐量。
并发数 (Concurrency):
- 定义:同一时刻,系统中正在处理的请求或活动的用户数量。
- 与性能的关系:高并发可能导致资源竞争加剧(CPU、内存、锁),对系统的处理能力和稳定性提出更高要求。
内存占用 (Memory Footprint):
- 定义:Java 应用程序运行时所占用的内存大小,主要指 Java 堆内存。
- 关注点:合理的内存占用,避免浪费,同时也要保证有足够的空间应对峰值负载和避免频繁 GC。
各项指标间的关系与权衡 (Trade-offs):
- 低延迟 vs. 高吞吐量:通常难以同时达到极致。追求极低 GC 停顿时间(低延迟)的收集器(如 ZGC)可能会牺牲一部分吞吐量,而追求高吞吐量的收集器(如 Parallel GC)可能会带来较长的 GC 停顿。需要根据应用的实际需求进行取舍。
- 内存占用 vs. 吞吐量/延迟:增大内存通常能降低 GC 频率,可能提升吞吐量或降低延迟,但会增加成本和单次 GC 的潜在时间(如果内存过大且 GC 算法不合适)。
- 并发数 vs. 响应时间/吞吐量:并发数增加到一定程度后,系统资源成为瓶颈,会导致响应时间变长,吞吐量可能下降。
类比高速公路:
- 吞吐量:每天通过收费站的车辆总数(或收取的总费用)。
- 并发数:某一时刻在高速公路上行驶的车辆数量。
- 响应时间(停顿时间):车辆通过某一段路程(或收费站)所需的时间,可以理解为车速。
- 内存占用:高速公路本身(车道数量、长度)。
提高吞吐量可能意味着需要修建更多车道(增加内存/CPU),但这可能在高峰期导致某些路段(GC)处理时间变长。限制车流速度(降低并发)可能保证单车通过时间短,但会降低整体通行量。JVM 调优就是在这些因素之间找到最佳平衡点。