JVM 性能监控
什么是 JVM 性能监控?
简单来说,JVM 性能监控就像是给 Java 程序做体检。就像医生通过检查血压、心跳、体温来判断人的健康状况一样,我们通过监控 JVM 的各种指标来判断 Java 程序是否健康运行。
为什么要做 JVM 性能监控?
想象一下,你的 Java 应用就像一辆汽车:
- 如果发动机(CPU)过热,车子会抛锚
- 如果油箱(内存)没油了,车子会熄火
- 如果轮胎(线程)磨损严重,车子会失控
JVM 性能监控就是帮我们提前发现这些问题,避免程序崩溃。
常用监控工具
jcmd
jcmd 是 JDK 7 引入的一个多功能诊断命令工具,它整合了 jps、jstack、jmap、jinfo 等工具的功能,是 JVM 诊断的瑞士军刀。jcmd 可以执行各种诊断命令,包括线程转储、堆转储、JFR 记录等。
基本语法
jcmd [options] <pid> <command> [arguments]
参数说明:
options:选项参数pid:Java 进程 ID(通过 jps 获取)command:要执行的命令arguments:命令参数
常用选项
# 列出所有可用的命令
jcmd 12345 help
# 列出所有 Java 进程
jcmd -l
# 连接到远程 JVM
jcmd host:port <command>
# 查看正在运行的JVM参数
jcmd pid VM.flags
#输出示例:
-XX:CICompilerCount=2 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=536870912 -XX:MaxNewSize=178913280 -XX:MinHeapDeltaBytes=196608 -XX:MinHeapSize=268435456 -XX:NewSize=89456640 -XX:NonNMethodCodeHeapSize=5826188 -XX:NonProfiledCodeHeapSize=122916026 -XX:OldSize=178978816 -XX:ProfiledCodeHeapSize=122916026 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:SoftMaxHeapSize=536870912 -XX:-THPStackMitigation -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC
JVM 参数详细解释
内存相关参数:
-XX:InitialHeapSize=268435456:初始堆大小 = 256MB (268435456 字节)-XX:MaxHeapSize=536870912:最大堆大小 = 512MB (536870912 字节)-XX:MinHeapSize=268435456:最小堆大小 = 256MB-XX:SoftMaxHeapSize=536870912:软最大堆大小 = 512MB
年轻代配置:
-XX:NewSize=89456640:年轻代初始大小 = 85.3MB-XX:MaxNewSize=178913280:年轻代最大大小 = 170.6MB
老年代配置:
-XX:OldSize=178978816:老年代大小 = 170.7MB
垃圾收集器配置:
-XX:+UseSerialGC:使用串行垃圾收集器(单线程 GC)-XX:MinHeapDeltaBytes=196608:堆大小调整的最小增量 = 192KB
代码缓存配置:
-XX:ReservedCodeCacheSize=251658240:代码缓存总大小 = 240MB-XX:+SegmentedCodeCache:启用分段代码缓存-XX:NonNMethodCodeHeapSize=5826188:非方法代码堆大小 = 5.6MB-XX:NonProfiledCodeHeapSize=122916026:非分析代码堆大小 = 117.2MB-XX:ProfiledCodeHeapSize=122916026:分析代码堆大小 = 117.2MB
编译器配置:
-XX:CICompilerCount=2:C1 编译器线程数 = 2 个
内存优化配置:
-XX:+UseCompressedOops:启用压缩对象指针(节省内存)-XX:+UseCompressedClassPointers:启用压缩类指针(节省内存)
系统配置:
-XX:-THPStackMitigation:禁用透明大页栈缓解(Linux 系统)
配置分析
优点:
- 内存配置合理:堆内存从 256MB 到 512MB,适合中小型应用
- 启用内存压缩:使用压缩指针节省内存空间
- 代码缓存充足:240MB 代码缓存足够 JIT 编译使用
潜在问题:
- 使用串行 GC:
UseSerialGC适合单核或内存很小的场景,多核环境下性能较差 - 堆内存较小:512MB 最大堆可能不够大型应用使用
- 编译器线程少:只有 2 个 C1 编译器线程,可能影响编译效率
建议优化:
- 对于多核环境,考虑使用
-XX:+UseG1GC或-XX:+UseParallelGC - 根据应用需求适当增加堆内存大小
- 考虑增加编译器线程数
-XX:CICompilerCount=4
这个配置看起来是一个相对保守的 JVM 配置,适合资源受限的环境或小型应用。
jps(Java Process Status)
# 查看所有Java进程(显示PID和主类名)
jps
# 输出示例:
# 12345 MyApplication
# 67890 SpringBootApp
# 11111 org.apache.tomcat.Main
# 查看所有 Java 进程(显示传递给 main 方法的参数)
jps -m
# 输出示例:
# 12345 MyApplication --server.port=8080
# 67890 SpringBootApp --spring.profiles.active=prod
# 查看所有Java进程(显示传递给JVM的参数)
jps -v
# 输出示例:
# 12345 MyApplication -Xms512m -Xmx1024m
# 67890 SpringBootApp -Dspring.profiles.active=prod
jstat(Java Statistics Monitoring Tool)
jstat 是 JVM 自带的统计监控工具,用于监控 JVM 的各种统计信息,包括内存使用、GC 情况、类加载等。
基本语法
jstat [options] <vmid> [interval] [count]
参数说明:
options:监控选项vmid:Java 进程 ID(通过 jps 获取)interval:监控间隔(毫秒)count:监控次数
常用监控选项
1. 监控 GC 情况
# 监控GC统计信息
jstat -gc 12345 1000 10
# 输出示例:
# S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
# 512.0 512.0 0.0 0.0 2048.0 1024.0 4096.0 2048.0 4480.0 4096.0 512.0 384.0 5 0.050 2 0.100 0.150
输出解释:
S0C/S1C:Survivor 0/1 区容量(KB)S0U/S1U:Survivor 0/1 区使用量(KB)EC:Eden 区容量(KB)EU:Eden 区使用量(KB)OC:老年代容量(KB)OU:老年代使用量(KB)MC:元空间容量(KB)MU:元空间使用量(KB)CCSC/CCSU:压缩类空间容量/使用量(KB)YGC:年轻代 GC 次数YGCT:年轻代 GC 总耗时(秒)FGC:Full GC 次数FGCT:Full GC 总耗时(秒)GCT:GC 总耗时(秒)
异常参数:
FGC > 10:Full GC 次数过多,可能存在内存泄漏FGCT > 5秒:Full GC 耗时过长,影响应用响应OU/OC > 0.9:老年代使用率过高,接近 OOMEU/EC > 0.95:Eden 区使用率过高,GC 频繁
解决方案:
- 内存泄漏:使用 jmap 导出堆转储,用 MAT 分析内存泄漏
- Full GC 耗时过长:调整 GC 参数,如
-XX:MaxGCPauseMillis=200 - 老年代使用率过高:增加堆内存
-Xmx,或优化代码减少对象创建 - Eden 区使用率过高:增加年轻代大小
-Xmn,或调整 Survivor 比例
2. 监控 GC 使用率
# 监控GC使用率
jstat -gcutil 12345 1000 10
# 输出示例:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 0.00 50.00 50.00 91.40 75.00 5 0.050 2 0.100 0.150
输出解释:
S0/S1:Survivor 0/1 区使用率(%)E:Eden 区使用率(%)O:老年代使用率(%)M:元空间使用率(%)CCS:压缩类空间使用率(%)YGC/YGCT:年轻代 GC 次数/耗时FGC/FGCT:Full GC 次数/耗时GCT:GC 总耗时
异常参数:
O > 85%:老年代使用率过高,需要关注O > 95%:老年代使用率极高,OOM 风险M > 90%:元空间使用率过高,可能存在类加载问题FGC > 1次/分钟:Full GC 频率过高GCT > 10%:GC 总耗时占比过高,影响性能
解决方案:
- 老年代使用率过高:增加堆内存,使用 G1GC,优化对象生命周期
- OOM 风险:立即增加
-Xmx,检查内存泄漏,考虑重启应用 - 元空间使用率过高:增加
-XX:MaxMetaspaceSize,检查动态类生成 - Full GC 频率过高:调整 GC 算法,优化内存分配
- GC 耗时占比过高:使用低延迟 GC(如 ZGC),优化 GC 参数
3. 监控内存使用
# 监控内存使用情况
jstat -memory 12345 1000 10
# 输出示例:
# NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC MCMN MCMX MC CCSMN CCSMX CCSC YGC FGC
# 512.0 1024.0 512.0 64.0 64.0 384.0 1024.0 4096.0 4096.0 4096.0 0.0 4480.0 4480.0 0.0 512.0 512.0 5 2
输出解释:
NGCMN/NGCMX/NGC:年轻代最小/最大/当前容量(KB)S0C/S1C:Survivor 0/1 区容量(KB)EC:Eden 区容量(KB)OGCMN/OGCMX/OGC:老年代最小/最大/当前容量(KB)OC:老年代容量(KB)MCMN/MCMX/MC:元空间最小/最大/当前容量(KB)CCSMN/CCSMX/CCSC:压缩类空间最小/最大/当前容量(KB)
异常参数:
NGC < NGCMN:年轻代容量异常,配置问题OGC < OGCMN:老年代容量异常,配置问题MC > MCMX * 0.9:元空间接近最大值CCSC > CCSMX * 0.9:压缩类空间接近最大值
解决方案:
- 年轻代容量异常:检查
-Xmn参数配置,确保合理设置 - 老年代容量异常:检查
-Xms和-Xmx参数,确保最小堆设置合理 - 元空间接近最大值:增加
-XX:MaxMetaspaceSize,检查类加载器泄漏 - 压缩类空间接近最大值:增加
-XX:CompressedClassSpaceSize,优化类结构
4. 监控类加载
# 监控类加载情况
jstat -class 12345 1000 10
# 输出示例:
# Loaded Bytes Unloaded Bytes Time
# 1234 2048.0 56 128.0 0.05
输出解释:
Loaded:已加载的类数量Bytes:已加载类的字节数(KB)Unloaded:已卸载的类数量Bytes:已卸载类的字节数(KB)Time:类加载耗时(秒)
异常参数:
Loaded > 10000:类加载数量过多,可能存在类加载器泄漏Unloaded > Loaded * 0.5:类卸载比例过高,可能存在类加载问题Time > 1秒:类加载耗时过长,影响启动性能Bytes > 100MB:类字节数过多,可能存在大量动态类生成
解决方案:
- 类加载数量过多:检查类加载器泄漏,使用 jmap 分析类加载器
- 类卸载比例过高:检查动态代理、反射等动态类生成,优化类加载策略
- 类加载耗时过长:使用类预加载,优化类路径,减少不必要的类加载
- 类字节数过多:检查动态类生成(如 CGLIB、Javassist),优化代码生成策略
5. 监控编译器
# 监控JIT编译器情况
jstat -compiler 12345 1000 10
# 输出示例:
# Compiled Failed Invalid Time FailedType FailedMethod
# 1234 0 0 0.05 0
输出解释:
Compiled:编译任务数Failed:编译失败数Invalid:无效编译数Time:编译耗时(秒)FailedType:失败类型FailedMethod:失败方法
异常参数:
Failed > 0:存在编译失败,需要关注Failed > Compiled * 0.1:编译失败率过高Invalid > 0:存在无效编译,可能存在代码问题Time > 10秒:编译耗时过长,影响性能FailedMethod > 0:存在方法编译失败,需要检查代码
解决方案:
- 编译失败:检查 JVM 版本兼容性,查看编译日志,更新 JVM
- 编译失败率过高:检查代码复杂度,优化热点方法,减少动态代码生成
- 无效编译:检查代码中的异常处理,优化 try-catch 块
- 编译耗时过长:调整编译阈值
-XX:CompileThreshold,使用分层编译 - 方法编译失败:检查方法大小限制,优化复杂方法,拆分大方法
jstack(Java Stack Trace)
jstack 是 JVM 自带的线程堆栈分析工具,用于生成 Java 进程中所有线程的堆栈跟踪信息,帮助诊断线程死锁、线程阻塞、CPU 使用率高等问题。
基本语法
jstack [options] <pid>
参数说明:
options:选项参数pid:Java 进程 ID(通过 jps 获取)
常用选项
# 基本用法:生成线程堆栈
jstack 12345
# 强制生成堆栈(即使进程挂起)
jstack -F 12345
# 生成长格式的堆栈信息
jstack -l 12345
# 生成长格式并强制生成
jstack -F -l 12345
输出示例及分析
1. 正常线程堆栈
jstack 12345
# 输出示例:
# 2024-01-15 10:30:45
# Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.291-b10 mixed mode):
#
# "main" #1 prio=5 os_prio=0 tid=0x00007f8c4c009000 nid=0x1234 runnable [0x00007f8c4d123000]
# java.lang.Thread.State: RUNNABLE
# at java.net.SocketInputStream.socketRead0(Native Method)
# at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
# at java.net.SocketInputStream.read(SocketInputStream.java:171)
# at java.net.SocketInputStream.read(SocketInputStream.java:141)
# at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
# at java.io.BufferedInputStream.read(BufferedInputStream.java:265)
# at java.io.ObjectInputStream$PeekInputStream.read(ObjectInputStream.java:2688)
# at java.io.ObjectInputStream$PeekInputStream.peek(ObjectInputStream.java:2702)
# at java.io.ObjectInputStream$BlockDataInputStream.peek(ObjectInputStream.java:2716)
# at java.io.ObjectInputStream$BlockDataInputStream.peekByte(ObjectInputStream.java:2726)
# at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1381)
# at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
# at com.example.MyApplication.main(MyApplication.java:25)
#
# "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f8c4c02a800 nid=0x1235 runnable
#
# "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f8c4c02c800 nid=0x1236 runnable
#
# "VM Thread" os_prio=0 tid=0x00007f8c4c025800 nid=0x1237 runnable
#
# "VM Periodic Task Thread" os_prio=0 tid=0x00007f8c4c02e800 nid=0x1238 waiting on condition
#
# JNI global references: 1234
输出解释:
"main":线程名称#1:线程编号prio=5:线程优先级os_prio=0:操作系统优先级tid=0x00007f8c4c009000:线程 IDnid=0x1234:本地线程 ID(对应操作系统线程 ID)runnable:线程状态[0x00007f8c4d123000]:线程栈地址
线程状态说明:
RUNNABLE:可运行状态,正在执行或等待 CPUBLOCKED:阻塞状态,等待获取锁WAITING:等待状态,等待其他线程通知TIMED_WAITING:限时等待状态TERMINATED:终止状态
2. 死锁检测
jstack -l 12345
# 输出示例(发现死锁):
# Found one Java-level deadlock:
# =============================
# "Thread-1":
# waiting to lock monitor 0x00007f8c4c123456 (object 0x00000000d1234567, a java.lang.Object),
# which is held by "Thread-2"
# "Thread-2":
# waiting to lock monitor 0x00007f8c4c123789 (object 0x00000000d1234568, a java.lang.Object),
# which is held by "Thread-1"
#
# Java stack information for the threads listed above:
# ===================================================
# "Thread-1":
# at com.example.DeadlockExample.methodA(DeadlockExample.java:25)
# - waiting to lock <0x00000000d1234567> (a java.lang.Object)
# at com.example.DeadlockExample.run(DeadlockExample.java:15)
# "Thread-2":
# at com.example.DeadlockExample.methodB(DeadlockExample.java:35)
# - waiting to lock <0x00000000d1234568> (a java.lang.Object)
# at com.example.DeadlockExample.run(DeadlockExample.java:20)
死锁分析:
- 死锁特征:两个或多个线程互相等待对方持有的锁
- 检测方法:使用
jstack -l查看锁信息 - 常见场景:数据库连接池、资源竞争、不当的锁顺序
3. 线程阻塞分析
jstack 12345
# 输出示例(线程阻塞):
# "http-nio-8080-exec-1" #23 daemon prio=5 os_prio=0 tid=0x00007f8c4c123456 nid=0x1239 waiting for monitor entry [0x00007f8c4d456000]
# java.lang.Thread.State: BLOCKED (on object monitor)
# at com.example.UserService.getUser(UserService.java:45)
# - waiting to lock <0x00000000d1234567> (a java.util.concurrent.ConcurrentHashMap)
# at com.example.UserController.getUser(UserController.java:25)
# at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
# at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
# at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
# at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handle(RequestMappingHandlerAdapter.java:134)
# at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
# at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
# at org.springframework.web.servlet.DispatcherServlet.service(DispatcherServlet.java:889)
# at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
# at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
# at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
# at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
# at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
# at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
# at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
# at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
# at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
# at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
# at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
# at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
# at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:860)
# at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1598)
# at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
# at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
# at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
# at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
# at java.lang.Thread.run(Thread.java:748)
阻塞分析:
- 阻塞特征:线程状态为
BLOCKED,等待获取锁 - 常见原因:锁竞争、数据库连接池耗尽、外部服务调用超时
- 影响:导致请求响应缓慢,甚至超时
4. CPU 使用率分析
# 结合 top 命令分析 CPU 使用率高的线程
top -H -p 12345
# 输出示例:
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 12345 java 20 0 1234567 123456 12345 R 95.2 2.3 0:15.23 java
# 然后使用 jstack 分析该线程
jstack 12345 | grep -A 20 "nid=0x1234"
CPU 使用率高的线程特征:
- 线程状态为
RUNNABLE - 长时间占用 CPU,没有 I/O 等待
- 可能存在死循环、复杂计算、频繁 GC
jmap(Java Memory Map)
jmap 是 JVM 自带的内存映射工具,用于生成 Java 进程的内存映射信息,包括堆内存使用情况、对象统计、类加载器信息等,是内存泄漏分析和性能调优的重要工具。
基本语法
jmap [options] <pid>
参数说明:
options:选项参数pid:Java 进程 ID(通过 jps 获取)
常用选项
# 显示堆内存使用情况
jmap -heap 12345
# 显示堆内存详细信息
jmap -histo 12345
# 显示堆内存详细信息(包含对象数量)
jmap -histo:live 12345
# 导出堆转储文件(用于内存泄漏分析)
jmap -dump:format=b,file=heap.hprof 12345
# 导出堆转储文件(只包含存活对象)
jmap -dump:live,format=b,file=heap_live.hprof 12345
# 显示类加载器信息
jmap -clstats 12345
# 显示 finalizer 队列信息
jmap -finalizerinfo 12345
输出示例及分析
1. 堆内存使用情况
jmap -heap 12345
# 输出示例:
# Attaching to process ID 12345, please wait...
# Debugger attached successfully.
# Server compiler detected.
# JVM version is 25.291-b10
#
# using thread-local object allocation.
# Parallel GC with 4 thread(s)
#
# Heap Configuration:
# MinHeapFreeRatio = 40
# MaxHeapFreeRatio = 70
# MaxHeapSize = 1073741824 (1024.0MB)
# NewSize = 357892096 (341.3MB)
# MaxNewSize = 357892096 (341.3MB)
# OldSize = 715849728 (682.7MB)
# NewRatio = 2
# SurvivorRatio = 8
# MetaspaceSize = 218103808 (208.0MB)
# CompressedClassSpaceSize = 1073741824 (1024.0MB)
# MaxMetaspaceSize = 17592186044415 MB
# G1HeapRegionSize = 0 (0.0MB)
#
# Heap Usage:
# PS Young Generation
# Eden Space:
# capacity = 268435456 (256.0MB)
# used = 134217728 (128.0MB)
# free = 134217728 (128.0MB)
# 50.0% used
# From Space:
# capacity = 44736512 (42.7MB)
# used = 0 (0.0MB)
# free = 44736512 (42.7MB)
# 0.0% used
# To Space:
# capacity = 44736512 (42.7MB)
# used = 0 (0.0MB)
# free = 44736512 (42.7MB)
# 0.0% used
# PS Old Generation
# capacity = 715849728 (682.7MB)
# used = 357924864 (341.3MB)
# free = 357924864 (341.3MB)
# 50.0% used
#
# 1234 interned Strings occupying 123456 bytes.
输出解释:
Heap Configuration:堆配置信息
MinHeapFreeRatio/MaxHeapFreeRatio:堆空闲比例范围MaxHeapSize:最大堆大小NewSize/MaxNewSize:年轻代大小OldSize:老年代大小NewRatio:年轻代与老年代比例SurvivorRatio:Eden 与 Survivor 比例MetaspaceSize:元空间大小CompressedClassSpaceSize:压缩类空间大小
Heap Usage:堆使用情况
capacity:容量used:已使用free:空闲- 使用率百分比
异常参数:
Eden Space used > 90%:Eden 区使用率过高,GC 频繁Old Generation used > 85%
实战监控案例
案例 1:内存泄漏排查
症状:应用运行一段时间后内存使用率持续上升,最终 OOM
排查步骤:
- 使用 jstat 观察内存变化趋势
jstat -gcutil pid 1000 10
- 使用 jmap 导出堆转储文件
jmap -dump:format=b,file=leak.hprof 12345
- 使用 MAT 分析堆转储文件,查找内存泄漏对象
解决方案:
- 修复代码中的内存泄漏点
- 增加堆内存大小
- 优化对象生命周期管理
脚本输出示例:
jstat -gcutil 12345 1000 10
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 0.00 95.00 85.00 91.40 75.00 50 1.200 5 2.100 3.300
# 0.00 0.00 98.00 90.00 92.00 76.00 51 1.250 5 2.100 3.350
输出解释:
E:Eden 区使用率(%),持续高于 90% 说明对象无法被及时回收O:老年代使用率(%),持续升高说明老年代对象不断累积FGC:Full GC 次数,频繁 Full GC 但内存未释放说明有泄漏
异常参数:
O > 85%且持续升高FGC频繁但O不下降
案例 2:CPU 使用率过高排查
症状:应用 CPU 使用率持续在 80% 以上,响应缓慢
排查步骤:
- 使用 top 命令找到高 CPU 进程
top -p 12345
- 使用 top -H 找到高 CPU 线程
top -H -p 12345
- 将线程 ID 转换为十六进制
printf "%x\n" 1234
# 输出:4d2
- 使用 jstack 分析线程堆栈
jstack 12345 | grep -A 20 "nid=0x4d2"
常见原因及解决方案:
- 死循环:检查循环条件,添加循环退出机制
- 复杂计算:优化算法,使用缓存,考虑异步处理
- 频繁 GC:调整 GC 参数,优化内存分配
- 线程竞争:减少锁竞争,使用无锁数据结构
脚本输出示例:
top -H -p 12345
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 12345 java 20 0 1234567 123456 12345 R 95.2 2.3 0:15.23 java
# 12346 java 20 0 1234567 123456 12345 R 90.0 2.3 0:14.00 java
输出解释:
%CPU:线程占用 CPU 百分比,持续高于 80% 为异常PID:可用于后续 jstack 分析
异常参数:
- 某线程
%CPU长时间 > 80% - 多个线程同时高 CPU
案例 3:线程死锁排查
症状:应用无响应,线程池耗尽,请求超时
排查步骤:
- 使用 jstack 检测死锁
jstack -l 12345
- 查找死锁信息
jstack -l 12345 | grep -A 50 "Found.*deadlock"
死锁示例代码:
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void methodA() {
synchronized (lock1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Method A executed");
}
}
}
public void methodB() {
synchronized (lock2) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Method B executed");
}
}
}
}
解决方案:
- 统一锁的获取顺序
- 使用
ReentrantLock的tryLock()方法 - 减少锁的粒度,使用分段锁
- 使用无锁数据结构
脚本输出示例:
jstack -l 12345
# Found one Java-level deadlock:
# =============================
# "Thread-1":
# waiting to lock monitor 0x00007f8c4c123456 (object 0x00000000d1234567, a java.lang.Object),
# which is held by "Thread-2"
# "Thread-2":
# waiting to lock monitor 0x00007f8c4c123789 (object 0x00000000d1234568, a java.lang.Object),
# which is held by "Thread-1"
输出解释:
Found one Java-level deadlock:检测到死锁waiting to lock:线程正在等待另一个线程持有的锁
异常参数:
- 出现
deadlock关键字 - 多个线程互相等待锁资源
案例 4:GC 频繁导致性能下降
症状:应用响应时间不稳定,GC 日志显示频繁的 Young GC
排查步骤:
- 使用 jstat 监控 GC 情况
jstat -gc 12345 1000 10
- 启用 GC 日志
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
- 分析 GC 日志
# GC 日志示例
2024-01-15T10:30:45.123+0800: [GC (Allocation Failure) [PSYoungGen: 262144K->32768K(305664K)] 262144K->32768K(1005056K), 0.050 secs] [Times: user=0.20 sys=0.00, real=0.05 secs]
异常指标:
- Young GC 频率 > 1 次/秒
- GC 耗时 > 100ms
- GC 后内存回收率 < 50%
解决方案:
- 增加年轻代大小:
-Xmn512m - 调整 Survivor 比例:
-XX:SurvivorRatio=8 - 使用 G1GC:
-XX:+UseG1GC - 优化对象分配,减少临时对象创建
脚本输出示例:
jstat -gc 12345 1000 10
# S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
# 512.0 512.0 0.0 0.0 2048.0 2047.0 4096.0 2048.0 4480.0 4096.0 512.0 384.0 50 1.200 2 0.800 2.000
# 512.0 512.0 0.0 0.0 2048.0 2047.0 4096.0 2048.0 4480.0 4096.0 512.0 384.0 51 1.250 2 0.800 2.050
输出解释:
YGC:年轻代 GC 次数,短时间内快速增长说明 GC 频繁YGCT:年轻代 GC 总耗时,耗时过高影响性能EU/EC:Eden 区使用率,接近 100% 说明 Eden 区频繁被填满
异常参数:
YGC> 1 次/秒YGCT> 100ms/次EU/EC > 0.95(Eden 区使用率过高)
案例 5:Full GC 频繁导致应用暂停
症状:应用偶尔出现长时间暂停,用户体验差
排查步骤:
- 监控 Full GC 频率
jstat -gcutil 12345 1000 10
- 分析 Full GC 原因
# 查看 GC 日志中的 Full GC 信息
grep "Full GC" gc.log
常见原因:
- 老年代内存不足
- 内存泄漏导致老年代对象无法回收
- 大对象直接进入老年代
解决方案:
- 增加堆内存:
-Xmx4g - 调整老年代比例:
-XX:NewRatio=3 - 使用 G1GC 减少暂停时间
- 排查并修复内存泄漏
脚本输出示例:
grep "Full GC" gc.log
# 2024-01-15T10:31:12.456+0800: [Full GC (Ergonomics) [PSYoungGen: 32768K->0K(305664K)] [ParOldGen: 682666K->341333K(682666K)] 715434K->341333K(988330K), [Metaspace: 123456K->123456K(123456K)], 0.8000000 secs] [Times: user=1.20 sys=0.00, real=0.80 secs]
输出解释:
Full GC:标识为 Full GC0.8000000 secs:本次 Full GC 耗时,超过 0.5 秒为异常ParOldGen: 682666K->341333K:老年代回收前后内存变化
异常参数:
- Full GC 耗时 > 0.5 秒
- Full GC 频率 > 1 次/5 分钟
- Full GC 后老年代回收率 < 50%
案例 6:类加载器泄漏排查
症状:元空间使用率持续增长,最终 OOM
排查步骤:
- 监控类加载情况
jstat -class 12345 1000 10
- 查看类加载器信息
jmap -clstats 12345
- 分析类加载器泄漏
jmap -dump:format=b,file=classloader.hprof 12345
常见原因:
- 动态代理类未正确卸载
- 反射生成的类未清理
- 自定义类加载器未正确关闭
解决方案:
- 及时清理动态代理对象
- 使用弱引用管理反射对象
- 正确关闭自定义类加载器
- 增加元空间大小:
-XX:MaxMetaspaceSize=512m
脚本输出示例:
jstat -class 12345 1000 10
# Loaded Bytes Unloaded Bytes Time
# 1234 2048.0 56 128.0 0.05
# 2234 3048.0 156 228.0 0.10
jmap -clstats 12345
# class_loader classes bytes parent_loader alive? type
# 0x00000000d1234567 1000 2048000 0x00000000d1234000 true java/lang/ClassLoader
# 0x00000000d1234568 500 1024000 0x00000000d1234000 true java/lang/ClassLoader
输出解释:
Loaded:已加载类数量,持续增长说明类未被卸载Unloaded:已卸载类数量,过低说明类加载器未回收class_loader:类加载器对象,数量异常多说明泄漏
异常参数:
Loaded持续增长且Unloaded很少- 类加载器对象数量异常多
- 元空间(Metaspace)使用率持续升高
案例 7:线程池耗尽排查
脚本输出示例:
jstack 12345 | grep -E "(pool|executor)" -A 5
# "taskExecutor-1" #23 prio=5 os_prio=0 tid=0x00007f8c4c123456 nid=0x1239 waiting on condition [0x00007f8c4d456000]
# java.lang.Thread.State: WAITING (parking)
# at sun.misc.Unsafe.park(Native Method)
# at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
# at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
# at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
输出解释:
Thread.State: WAITING (parking):线程处于等待任务状态ThreadPoolExecutor.getTask:线程池队列已满,线程在等待新任务
异常参数:
- 线程池中大量线程处于 WAITING 状态
- 队列长度接近或等于最大容量
- 任务堆积,响应超时
案例 8:数据库连接池耗尽排查
脚本输出示例:
jstack 12345 | grep -E "(mysql|postgres|oracle)" -A 10
# "HikariPool-1 housekeeper" #45 prio=5 os_prio=0 tid=0x00007f8c4c123789 nid=0x1240 waiting on condition [0x00007f8c4d789000]
# java.lang.Thread.State: WAITING (on object monitor)
# at java.lang.Object.wait(Native Method)
# at com.zaxxer.hikari.pool.HikariPool$HouseKeeper.run(HikariPool.java:1234)
# ...
输出解释:
Thread.State: WAITING (on object monitor):线程在等待数据库连接释放HikariPool$HouseKeeper:连接池维护线程,可能因无可用连接而阻塞
异常参数:
- 连接池线程长时间等待
- 活跃连接数接近最大池大小
- 数据库响应慢或超时
案例 9:内存碎片化排查
脚本输出示例:
jmap -heap 12345
# PS Old Generation
# capacity = 715849728 (682.7MB)
# used = 341333333 (325.6MB)
# free = 374516395 (357.1MB)
# 50% used
输出解释:
capacity:老年代总容量used:已用内存free:空闲内存used占比不高但频繁 Full GC,说明内存碎片化严重
异常参数:
- 老年代
used< 70% 但频繁 Full GC - 大量小对象分布,回收效果差
案例 10:网络 I/O 阻塞排查
脚本输出示例:
jstack 12345 | grep -E "(http|nio|socket)" -A 10
# "http-nio-8080-exec-1" #23 daemon prio=5 os_prio=0 tid=0x00007f8c4c123456 nid=0x1239 waiting on condition [0x00007f8c4d456000]
# java.lang.Thread.State: WAITING (on object monitor)
# at java.net.SocketInputStream.socketRead0(Native Method)
# at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
# ...
输出解释:
Thread.State: WAITING (on object monitor):线程在等待网络 I/OSocketInputStream.socketRead:同步 I/O 阻塞,线程无法继续执行
异常参数:
- 大量线程处于 WAITING 状态
- 网络 I/O 调用堆栈频繁出现
案例 11:缓存穿透导致性能问题
脚本输出示例:
tail -f cache.log | grep "MISS"
# [2024-01-15 10:32:00] cache MISS: user:123
# [2024-01-15 10:32:01] cache MISS: user:124
输出解释:
cache MISS:缓存未命中,频繁 MISS 说明缓存穿透
异常参数:
- 缓存命中率 < 80%
- 相同 key 频繁 MISS
- 数据库压力异常升高
案例 12:JVM 参数配置不当
脚本输出示例:
jcmd 12345 VM.flags
# -Xms1g -Xmx1g -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:+UseSerialGC -XX:MaxMetaspaceSize=128m
输出解释:
-Xms/-Xmx:堆内存过小-XX:NewRatio、-XX:SurvivorRatio:比例不合理-XX:+UseSerialGC:单线程 GC,性能差-XX:MaxMetaspaceSize:元空间过小
异常参数:
- 堆内存 < 2G
- 年轻代/老年代比例极端
- Survivor 区比例过小
- 使用 SerialGC
- 元空间 < 256M
案例 13:应用启动慢排查
脚本输出示例:
tail -f application.log | grep -E "(Started|Initialized)"
# 2024-01-15 10:33:00 Application Started in 35.123 seconds
输出解释:
Started in xx seconds:启动耗时,超过 30 秒为异常
异常参数:
- 启动时间 > 30 秒
- 类加载数 > 10000
- 启动期间 GC 频繁
案例 14:内存使用率突增排查
脚本输出示例:
jstat -gcutil 12345 1000 5
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 0.00 95.00 85.00 91.40 75.00 50 1.200 5 2.100 3.300
# 0.00 0.00 98.00 90.00 92.00 76.00 51 1.250 5 2.100 3.350
输出解释:
E、O:Eden 区、老年代使用率短时间内急剧上升YGC、FGC:GC 次数短时间内激增
异常参数:
- 内存使用率短时间内 > 90%
- GC 次数短时间内激增
案例 15:应用响应时间不稳定
脚本输出示例:
jstat -gcutil 12345 1000 10
# GCT
# 0.150
# 0.300
# 0.800
输出解释:
GCT:GC 总耗时,波动大说明 GC 影响响应
异常参数:
- GC 总耗时波动大
- 线程状态频繁切换(RUNNABLE/BLOCKED/WAITING)
- 系统资源利用率波动大
