JVM 线上调优与排查指南

文章目录


第一章:工具概览

1.1 三大诊断工具对比

工具全名主要功能输出内容使用场景
jstackStack Trace for Java线程堆栈分析线程状态、调用栈、锁信息、死锁检测死锁检测、CPU飙高定位、线程阻塞分析
jstatJVM Statistics MonitoringJVM统计监控GC次数、耗时、内存使用率、类加载GC监控、内存使用率监控、性能趋势分析
jmapMemory Map for Java内存映射分析堆配置、对象分布、堆转储文件内存泄漏分析、OOM问题排查、大对象查找
jconsoleJava Console图形化监控实时监控界面、可视化图表日常监控、问题排查、性能分析

1.2 工具选择指南

问题类型首选工具辅助工具关键命令
死锁jstackjconsolejstack -l <pid>
CPU高jstack + topjconsoletop -H + jstack
频繁GCjstatjconsolejstat -gcutil <pid> 1000
内存泄漏jmapMATjmap -histo + dump
OOMjmapMATjmap -heap + dump
响应慢jstackjstatjstack(查看BLOCKED)
日常监控jconsolejstat图形化界面

第二章:jstack - 线程堆栈分析

2.1 核心功能

死锁检测 - 自动识别并报告死锁
CPU飙高定位 - 配合top找到具体代码行
线程状态分析 - 查看所有线程的运行状态
阻塞分析 - 找到被阻塞的线程和原因

2.2 命令语法

jstack [options] <pid>

选项:
  -l    打印锁的额外信息
  -F    强制dump(当进程挂起时)
  -m    打印java和native的堆栈信息
  -h    帮助信息

2.3 实用示例

示例1:检测死锁
# 运行死锁程序
java diagnostic.DeadLockDemo

# 使用jstack检测
jstack <pid>

# 输出会自动显示:
Found one Java-level deadlock:
=============================
"Thread-2":
  waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
  which is held by "Thread-1"
...
Found 1 deadlock.
示例2:定位CPU飙高的线程
# 步骤1:找到Java进程PID
jps -l
# 输出: 12345 diagnostic.HighCPUDemo

# 步骤2:找到占用CPU最高的线程
top -H -p 12345
# 找到TID=12346占用CPU 98%

# 步骤3:转换TID为16进制
printf "%x\n" 12346
# 输出: 303a

# 步骤4:用jstack查看该线程
jstack 12345 | grep -A 30 303a
# 输出:
"High-CPU-Thread" #12 nid=0x303a runnable
  at diagnostic.HighCPUDemo.lambda$main$1(HighCPUDemo.java:27)
  ↑ 定位到具体代码行!
示例3:统计线程状态
# 统计各状态线程数量
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c

# 输出示例:
  50 java.lang.Thread.State: RUNNABLE
 200 java.lang.Thread.State: WAITING
  10 java.lang.Thread.State: TIMED_WAITING
   5 java.lang.Thread.State: BLOCKED  ← 如果这个数量多,说明有锁竞争

2.4 线程状态说明

状态说明可能原因正常比例
RUNNABLE运行中正常执行10-30%
BLOCKED阻塞等待获取锁<5%
WAITING等待wait()、park()50-80%
TIMED_WAITING限时等待sleep()、wait(timeout)10-30%
TERMINATED终止线程结束-

第三章:jstat - JVM统计监控

3.1 核心功能

GC监控 - 实时监控GC次数和耗时
内存使用率 - 查看各区域使用率
类加载统计 - 类加载/卸载情况
性能趋势 - 持续监控,发现趋势

3.2 命令语法

jstat [option] <pid> [interval] [count]

参数:
  option    统计选项(-gc, -gcutil等)
  pid       进程ID
  interval  刷新间隔(毫秒)
  count     刷新次数

常用选项:
  -gc          GC统计
  -gcutil      GC统计(百分比)
  -gccause     GC原因
  -gcnew       新生代统计
  -gcold       老年代统计
  -class       类加载统计
  -compiler    JIT编译统计

3.3 实用示例

示例1:实时监控GC
# 每秒刷新一次,共60次(监控1分钟)
jstat -gcutil <pid> 1000 60

# 输出示例:
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00  28.50  45.20  35.60  85.30  80.10    120    1.234     5    0.567    1.801
  0.00  35.20  52.30  35.80  85.30  80.10    121    1.245     5    0.567    1.812
 12.50   0.00  15.40  36.20  85.35  80.10    122    1.253     5    0.567    1.820
        ↑ 发生了Minor GC,对象从S1复制到S0

# 关键指标:
# - E(Eden)接近100%时会触发Minor GC
# - O(老年代)持续增长可能有内存泄漏
# - YGC增长过快说明对象分配频繁
# - FGC频繁说明老年代压力大
示例2:分析GC原因
jstat -gccause <pid> 1000

# 输出:
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC                 
  0.00  28.57  50.00   7.06  96.54  90.00    15    0.125     2    0.089    0.214  Allocation Failure   No GC
                                                                                   ↑ 上次GC原因           ↑ 当前GC原因

# 常见GC原因:
# - Allocation Failure: 分配内存失败(正常)
# - System.gc(): 显式调用System.gc()(应避免)
# - Metadata GC Threshold: 元空间达到阈值
# - Ergonomics: 自适应调整

3.4 输出列说明

-gc 输出
说明单位
S0CSurvivor0容量KB
S1CSurvivor1容量KB
S0USurvivor0使用量KB
S1USurvivor1使用量KB
ECEden容量KB
EUEden使用量KB
OC老年代容量KB
OU老年代使用量KB
MC元空间容量KB
MU元空间使用量KB
YGCYoung GC次数
YGCTYoung GC总耗时
FGCFull GC次数
FGCTFull GC总耗时
GCT总GC耗时
-gcutil 输出
说明范围
S0Survivor0使用率0-100%
S1Survivor1使用率0-100%
EEden使用率0-100%
O老年代使用率0-100%
M元空间使用率0-100%

3.5 性能指标参考

指标良好警告严重
Young GC频率<5次/分5-10次/分>10次/分
Young GC耗时<50ms50-100ms>100ms
Full GC频率<1次/小时1-5次/小时>5次/小时
Full GC耗时<500ms0.5-1s>1s
GC总耗时占比<1%1-5%>5%

第四章:jmap - 内存映射分析

4.1 核心功能

堆配置查看 - 查看堆大小、GC配置
对象统计 - 查看对象数量和占用空间
内存泄漏分析 - 找出占用内存最多的对象
堆转储 - 生成heap dump文件供MAT分析

4.2 命令语法

jmap [options] <pid>

选项:
  -heap                 打印堆配置和使用情况
  -histo                打印对象统计
  -histo:live           打印存活对象统计(触发GC)
  -dump:<dump-options>  生成堆转储文件
  -F                    强制执行(进程挂起时)

dump选项:
  format=b              二进制格式
  file=<file>           输出文件
  live                  只dump存活对象

4.3 实用示例

示例1:查看堆配置
jmap -heap <pid>

# 输出示例:
Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 268435456 (256.0MB)  ← 最大堆
   NewSize                  = 89128960 (85.0MB)    ← 新生代
   MaxNewSize               = 89128960 (85.0MB)
   OldSize                  = 179306496 (171.0MB)  ← 老年代
   NewRatio                 = 2                    ← 新生代:老年代 = 1:2
   SurvivorRatio            = 8                    ← Eden:Survivor = 8:1
   MetaspaceSize            = 21807104 (20.8MB)
   MaxMetaspaceSize         = unlimited

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 67108864 (64.0MB)
   used     = 32145678 (30.65MB)
   free     = 34963186 (33.35MB)
   47.89% used  ← Eden使用率

PS Old Generation
   capacity = 179306496 (171.0MB)
   used     = 125829120 (120.0MB)
   free     = 53477376 (51.0MB)
   70.18% used  ← 老年代使用率(如果>80%需要关注)
示例2:查找大对象
# 查看对象分布(前30个)
jmap -histo <pid> | head -30

# 输出示例:
 num     #instances         #bytes  class name
----------------------------------------------
   1:         50000      120000000  [B                      ← byte数组,占用114MB
   2:        100000       32000000  java.lang.String        ← String,占用30MB
   3:         12345       12345678  java.util.HashMap$Node
   4:          5000       10000000  [C                      ← char数组
   5:          2000        8000000  com.example.LargeObject ← 自定义类

# 查找特定类型
jmap -histo <pid> | grep "\[B"           # 所有byte数组
jmap -histo <pid> | grep "com.example"   # 自己的包

# 对象类型代码:
# [B = byte[]
# [C = char[]
# [I = int[]
# [L = Object[]
示例3:生成堆转储文件
# 生成完整堆转储
jmap -dump:format=b,file=heap_full.hprof <pid>

# 生成存活对象堆转储(推荐,文件更小)
jmap -dump:live,format=b,file=heap_live.hprof <pid>

# 输出:
Dumping heap to /path/to/heap_live.hprof ...
Heap dump file created [234567890 bytes in 3.5 secs]

# 分析heap dump文件:
# 1. 下载 Eclipse MAT: https://www.eclipse.org/mat/
# 2. 打开 heap_live.hprof
# 3. 查看 Leak Suspects Report(内存泄漏疑点)
# 4. 查看 Dominator Tree(支配树,找最大对象)
# 5. 查看 Histogram(对象直方图)

4.4 对象类型代码

代码类型
Bbyte
Cchar
Ddouble
Ffloat
Iint
Jlong
Zboolean
[数组
L对象

4.5 内存使用指标

区域健康警告危险
Eden使用率<80%80-95%>95%
老年代使用率<70%70-85%>85%
元空间使用率<80%80-90%>90%
堆总使用率<75%75-90%>90%

第五章:jconsole - 图形化监控

5.1 启动和连接

启动JConsole
# 方法1:直接启动JConsole
jconsole

# 方法2:指定Java进程
jconsole PID

# 方法3:指定主机和端口
jconsole hostname:port

# 方法4:通过JAVA_HOME启动
$JAVA_HOME/bin/jconsole
连接方式
# 本地连接(推荐)
1. 启动JConsole
2. 选择"本地进程"
3. 选择目标Java进程
4. 点击"连接"

# 远程连接
1. 启动JConsole
2. 选择"远程进程"
3. 输入:hostname:port
4. 点击"连接"

# 远程连接需要JMX配置
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

5.2 主要功能模块

Overview(概览)
功能:显示JVM基本信息和关键指标
包含:
✅ 堆内存使用情况
✅ 非堆内存使用情况
✅ 线程数量
✅ 类加载数量
✅ CPU使用率
✅ GC信息
Memory(内存)
功能:详细的内存使用情况监控
包含:
✅ 堆内存各区域使用情况
✅ 非堆内存使用情况
✅ 内存使用趋势图
✅ 垃圾回收信息

区域说明:
- Eden Space:新生代Eden区
- Survivor Space:新生代Survivor区
- Old Gen:老年代
- Metaspace:元空间(JDK 8+)
- Code Cache:代码缓存
- Compressed Class Space:压缩类空间
Threads(线程)
功能:线程状态监控和管理
包含:
✅ 线程数量统计
✅ 线程状态分布
✅ 线程详细信息
✅ 死锁检测
✅ 线程堆栈信息

线程状态:
- RUNNABLE:运行中
- BLOCKED:阻塞
- WAITING:等待
- TIMED_WAITING:超时等待
- TERMINATED:已终止
Classes(类)
功能:类加载情况监控
包含:
✅ 已加载类数量
✅ 已卸载类数量
✅ 类加载趋势
✅ 类加载器信息
VM Summary(虚拟机摘要)
功能:JVM配置和运行信息
包含:
✅ JVM版本信息
✅ 启动参数
✅ 系统属性
✅ 运行时间
✅ 编译信息
MBean(管理Bean)
功能:JMX管理功能
包含:
✅ 内存管理
✅ 垃圾回收管理
✅ 线程管理
✅ 类加载管理
✅ 自定义MBean

5.3 使用技巧

内存监控技巧
# 1. 监控堆内存使用趋势
- 观察Eden Space的使用情况
- 关注Old Gen的增长趋势
- 监控Survivor Space的波动

# 2. 内存泄漏检测
- 观察Old Gen是否持续增长
- 检查GC后内存是否释放
- 关注Metaspace的使用情况

# 3. 内存优化建议
- 如果Eden Space频繁满,考虑增加新生代大小
- 如果Old Gen增长过快,检查对象生命周期
- 如果Metaspace使用过多,检查类加载情况
线程监控技巧
# 1. 死锁检测
- 查看"检测死锁"按钮
- 死锁线程会显示为红色
- 点击查看死锁详情

# 2. 线程状态分析
- RUNNABLE线程过多:可能CPU使用率高
- BLOCKED线程过多:可能存在锁竞争
- WAITING线程过多:可能等待资源

# 3. 线程堆栈分析
- 双击线程查看堆栈信息
- 关注阻塞的线程
- 分析线程等待的原因

第六章:其他诊断工具

6.1 jps - Java进程查看

基本用法
# 列出所有Java进程
jps

# 列出所有Java进程的详细信息
jps -l

# 列出所有Java进程的完整信息
jps -v

# 列出所有Java进程的完整信息
jps -m
输出示例
$ jps -l
12345 com.example.Application
67890 org.springframework.boot.loader.JarLauncher

6.2 jinfo - JVM参数查看和修改

基本用法
# 查看JVM参数
jinfo <pid>

# 查看特定JVM参数
jinfo -flag MaxHeapSize <pid>

# 动态修改JVM参数
jinfo -flag +PrintGC <pid>
输出示例
$ jinfo 12345
Attaching to process ID 12345, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.1+13-LTS
Java System Properties:
java.vm.name = OpenJDK 64-Bit Server VM
java.vm.version = 11.0.1+13-LTS
java.vm.vendor = Oracle Corporation

6.3 jcmd - 综合诊断工具

基本用法
# 列出所有可用的命令
jcmd <pid> help

# 生成堆转储文件
jcmd <pid> GC.run_finalization

# 生成线程转储文件
jcmd <pid> Thread.print

# 生成类直方图
jcmd <pid> GC.class_histogram

# 查看JVM参数
jcmd <pid> VM.flags

# 查看系统属性
jcmd <pid> VM.system_properties

# 查看命令行参数
jcmd <pid> VM.command_line

# 查看JVM版本信息
jcmd <pid> VM.version

# 查看JVM启动时间
jcmd <pid> VM.uptime

第七章:线上问题诊断

7.1 死锁检测

7.1.1 现象判别
  • 业务线程数正常但接口永久阻塞;日志停在某一步骤不再推进。
  • top/监控中 CPU 反而偏低,线程数稳定但无进展。
  • jstack 多次抓取相同线程都卡在 BLOCKED (on object monitor),堆栈重复。
7.1.2 快速定位流程
  1. top -H -p <pid> 或 Arthas thread -n 5 初步查看是否有大量线程处于 BLOCKED
  2. jstack <pid> | grep -A 30 <hexTid> 定位互相等待的线程与锁;或 Arthas thread -b 输出阻塞线程。
  3. jconsole 线程面板的“检测死锁”可一键验证,死锁线程会标红。
  4. 若确认死锁,保留堆栈/日志,排查代码中的双重锁序、嵌套锁或资源竞争。
7.1.3 常见根因方向
  • 互斥锁获取顺序不一致(线程A先锁资源1再锁资源2,线程B顺序相反)
  • 嵌套锁/递归锁未妥善释放,导致线程间循环等待
  • 数据库连接、分布式锁等外部资源获取顺序冲突
  • synchronized 与显式锁(ReentrantLock、ReadWriteLock等)混合使用不当
  • 异常路径缺失 finally 解锁,或调用 wait/notify 时持锁不释放
实时死锁检测
# 1. 使用jstack检测死锁
jstack PID | grep -A 10 -B 10 "deadlock"

# 2. 使用jcmd检测死锁
jcmd PID Thread.print | grep -A 20 -B 5 "deadlock"

# 3. 使用jconsole检测
# 启动jconsole,连接进程,查看"线程"标签页
# 死锁线程会显示为红色
死锁检测脚本
#!/bin/bash
# deadlock-detector.sh

PID=$1
if [ -z "$PID" ]; then
    echo "Usage: $0 <PID>"
    exit 1
fi

echo "检测进程 $PID 的死锁情况..."

# 检测死锁
DEADLOCK_INFO=$(jstack $PID | grep -A 20 "Found Java-level deadlock")

if [ -n "$DEADLOCK_INFO" ]; then
    echo "🚨 发现死锁!"
    echo "$DEADLOCK_INFO"
    
    # 发送告警
    curl -X POST "webhook_url" -d "{
        \"text\": \"发现死锁: PID=$PID\",
        \"deadlock_info\": \"$DEADLOCK_INFO\"
    }"
else
    echo "✅ 未发现死锁"
fi

# 检测线程阻塞
BLOCKED_THREADS=$(jstack $PID | grep "BLOCKED" | wc -l)
if [ $BLOCKED_THREADS -gt 10 ]; then
    echo "⚠️  发现 $BLOCKED_THREADS 个阻塞线程,可能存在死锁风险"
fi

7.2 CPU高检测

7.2.1 现象判别
  • 监控/top 显示 Java 进程 %CPU 持续高位,系统负载升高。
  • 单条线程 CPU 飙高:top -H -p <pid> 发现某 TID 长期占 90%+。
  • GC 线程占用高时需结合 GC 日志确认是否因频繁 GC。
7.2.2 快速定位流程
  1. top -H -p <pid>ps -Lp <pid> 找到占用高的线程 ID,转换为 16 进制(printf "%x\n" <tid>)。
  2. jstack <pid> | grep -A 30 <hexTid> 查看堆栈,定位具体代码行;或 Arthas thread -n 5thread <tid>
  3. 使用 Arthas 深入:
    • trace com.xxx.Service method "#cost > 100" 追踪高耗时方法。
    • watch com.xxx.Service method "{params,#cost}" 观察入参/耗时。
    • profiler startprofiler stop --format html 生成 CPU 火焰图。
  4. 结合业务逻辑排查:死循环、锁自旋、热点 SQL/远程调用、多线程竞争等。
7.2.3 常见根因方向
  • 代码逻辑上的死循环、忙轮询、递归无退出
  • 热点算法/正则回溯/字符串拼接造成高 CPU 计算
  • 锁竞争或 CAS 自旋导致线程长时间空转
  • GC 频繁(参数不当或对象激增)引发 GC 线程占用 CPU
  • 外部接口/数据库耗时长,工作线程大量堆积在 RUNNABLE
CPU使用率监控
# 1. 实时监控CPU使用率
top -p PID -n 1 | grep PID

# 2. 监控Java进程CPU使用率
ps -p PID -o pid,ppid,cmd,%cpu,%mem

# 3. 使用htop查看详细CPU信息
htop -p PID
CPU高问题诊断脚本
#!/bin/bash
# cpu-monitor.sh

PID=$1
THRESHOLD=80  # CPU使用率阈值

if [ -z "$PID" ]; then
    echo "Usage: $0 <PID>"
    exit 1
fi

while true; do
    # 获取CPU使用率
    CPU_USAGE=$(ps -p $PID -o %cpu --no-headers | tr -d ' ')
    
    if (( $(echo "$CPU_USAGE > $THRESHOLD" | bc -l) )); then
        echo "$(date): 🚨 CPU使用率过高: ${CPU_USAGE}%"
        
        # 生成线程堆栈
        jstack $PID > "jstack-$(date +%Y%m%d-%H%M%S).txt"
        
        # 分析CPU热点
        echo "=== CPU热点分析 ==="
        jstack $PID | grep -A 5 -B 5 "RUNNABLE" | head -20
        
        # 发送告警
        curl -X POST "webhook_url" -d "{
            \"text\": \"CPU使用率过高: ${CPU_USAGE}%\",
            \"pid\": \"$PID\",
            \"timestamp\": \"$(date)\"
        }"
    fi
    
    sleep 10
done

7.3 内存高检测

7.3.1 现象判别
  • 监控中堆使用率持续拉高,GC 后下降不明显,Full GC 频率增加。
  • 触发 OOM 前 RSS/堆曲线呈“锯齿向上”;jstat -gcutil Old Gen 值逐步逼近 100%。
  • Arthas memoryjmap -heap 显示老年代接近满载。
7.3.2 快速定位流程
  1. jstat -gcutil <pid> 1000 观察 Eden/Old 使用率与 GC 次数,判断是“瞬时膨胀”还是“无法回收”。
  2. jmap -histo <pid> 连续执行两次,比较对象 Top 列表是否持续增长。
  3. jmap -dump:live 或 Arthas heapdump 导出堆,配合 MAT/VisualVM/HeapHero 分析 Dominator Tree、Leak Suspects。
  4. 使用 Arthas 辅助:
    • vmtool --action getInstances --className xxx 查看实例数。
    • watch/trace 跟踪可疑方法的对象创建。
  5. 根据结果采取措施:清理缓存/集合、修复 ThreadLocal、检查大对象生命周期、优化分配模式。
7.3.3 常见根因方向
  • 静态集合、缓存、Map 无上限增长,缺少过期/淘汰策略
  • 短时间突发流量或批处理任务创建大量对象导致堆膨胀
  • 线程池/消息队列堆积,任务数据在内存中等待处理
  • 序列化/反序列化、大文件读取一次性加载全部数据
  • GC 参数配置不当(Survivor 太小、晋升阈值低)导致老年代被挤爆
内存使用率监控
# 1. 监控堆内存使用率
jstat -gc PID 1s 5

# 2. 监控进程内存使用
ps -p PID -o pid,ppid,cmd,%mem,rss,vsz

# 3. 监控系统内存使用
free -h
内存高问题诊断脚本
#!/bin/bash
# memory-monitor.sh

PID=$1
THRESHOLD=85  # 内存使用率阈值

if [ -z "$PID" ]; then
    echo "Usage: $0 <PID>"
    exit 1
fi

while true; do
    # 获取堆内存使用率
    HEAP_USAGE=$(jstat -gc $PID | tail -1 | awk '{print ($3+$4+$6+$8)/($1+$2+$5+$7)*100}')
    
    if (( $(echo "$HEAP_USAGE > $THRESHOLD" | bc -l) )); then
        echo "$(date): 🚨 堆内存使用率过高: ${HEAP_USAGE}%"
        
        # 生成堆转储
        jmap -dump:live,format=b,file="heap-$(date +%Y%m%d-%H%M%S).hprof" $PID
        
        # 分析内存使用
        echo "=== 内存使用分析 ==="
        jmap -histo $PID | head -20
        
        # 发送告警
        curl -X POST "webhook_url" -d "{
            \"text\": \"堆内存使用率过高: ${HEAP_USAGE}%\",
            \"pid\": \"$PID\",
            \"timestamp\": \"$(date)\"
        }"
    fi
    
    sleep 30
done

7.4 内存泄漏检测

7.4.1 现象判别
  • GC 后 Old Gen 仍不下降,jstat 多次输出 OG 使用率逐渐逼近 100%。
  • 日志出现 java.lang.OutOfMemoryError 之前伴随 Full GC、频繁 Promotion failed
  • MAT/HeapHero 报告中存在长时间不释放的大对象或静态集合。
7.4.2 快速定位流程
  1. 先确认现象:jstat -gcutil <pid>、监控平台堆曲线、GC 日志。
  2. 导出堆:jmap -dump:live,format=b,file=heap.hprof <pid>(注意对线上性能影响)。
  3. 使用 MAT 分析:
    • Leak Suspects 报告、Dominator Tree 找“谁持有最多内存”。
    • Path to GC Roots 分析引用链(常见为静态集合、缓存、ThreadLocal、监听器)。
  4. 在线追踪:
    • Arthas watch com.xxx.Service create "{params,returnObj}" 观察对象创建频次。
    • profiler start --event alloc 捕获分配热点。
  5. 制定修复方案:清理缓存/集合、增加过期策略、确保资源关闭、避免泄漏型单例。
7.4.3 常见根因方向
  • 缓存、集合、Session 等容器持续增长,无淘汰清理
  • ThreadLocal 使用后未 remove,在线程复用场景中残留引用
  • 监听器/回调注册后未注销,导致对象被长期持有
  • 单例或静态变量持有外部资源(上下文、连接、对象图)
  • 自定义类加载器/动态代理反复生成类,导致 ClassLoader 泄漏
内存泄漏检测脚本
#!/bin/bash
# memory-leak-detector.sh

PID=$1
if [ -z "$PID" ]; then
    echo "Usage: $0 <PID>"
    exit 1
fi

# 生成两个时间点的堆转储
echo "生成第一个堆转储..."
jmap -dump:live,format=b,file="heap-before-$(date +%Y%m%d-%H%M%S).hprof" $PID

echo "等待5分钟..."
sleep 300

echo "生成第二个堆转储..."
jmap -dump:live,format=b,file="heap-after-$(date +%Y%m%d-%H%M%S).hprof" $PID

# 分析内存增长
echo "=== 内存增长分析 ==="
jmap -histo $PID | head -20

# 检查GC情况
echo "=== GC情况分析 ==="
jstat -gc $PID

第八章:实战案例

8.1 案例1:应用响应慢

症状:

  • 接口响应时间从100ms增加到5s
  • 用户投诉系统卡顿

排查步骤:

# 1. 检查CPU使用率
top -p <pid>
# 如果CPU很高 → 去案例2
# 如果CPU正常,IO高 → 可能是数据库或网络问题
# 如果CPU和IO都正常 → 可能是线程阻塞

# 2. 查看线程状态
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c
# 输出:
#  50 RUNNABLE
# 200 WAITING
#  80 BLOCKED  ← 大量阻塞!
#  20 TIMED_WAITING

# 3. 查看阻塞的线程
jstack <pid> | grep -A 10 "BLOCKED"
# 找到被阻塞的线程和等待的锁

# 4. 分析锁竞争
jstack -l <pid> > thread_dump.txt
# 查看哪个线程持有锁,为什么不释放

8.2 案例2:CPU飙高

症状:

  • CPU使用率持续90%以上
  • 应用响应缓慢

排查步骤:

# 1. 找到高CPU线程
top -H -p <pid>
# 记录TID(如12346)

# 2. 转换为16进制
printf "%x\n" 12346
# 输出: 303a

# 3. 查看该线程堆栈
jstack <pid> | grep -A 30 303a

# 4. 定位代码
# 在堆栈中找到自己的代码
at com.example.service.UserService.queryUsers(UserService.java:45)
#                                                                  ↑ 第45行

# 5. 分析代码
# - 是否有死循环
# - 是否有大量计算
# - 是否有正则表达式回溯
# - 是否有大量字符串拼接

8.3 案例3:频繁Full GC

症状:

  • 应用频繁卡顿
  • 日志中大量Full GC

排查步骤:

# 1. 监控GC(持续5分钟)
jstat -gcutil <pid> 1000 300 > gc.log

# 2. 分析gc.log
# 关注:
# - FGC列是否快速增长
# - O(老年代)使用率是否持续很高
# - FGCT(Full GC总耗时)是否占比很大

# 3. 查看堆配置
jmap -heap <pid>
# 检查堆大小是否合理

# 4. 查看对象分布
jmap -histo <pid> | head -30
# 找出占用内存最多的对象

# 5. 生成堆转储分析
jmap -dump:live,format=b,file=heap.hprof <pid>

# 6. 使用MAT分析
# - 查看 Leak Suspects(泄漏疑点)
# - 查看 Dominator Tree(找大对象)
# - 查看 Path to GC Roots(找引用链)

8.4 案例4:内存溢出

症状:

  • 应用崩溃
  • 日志:java.lang.OutOfMemoryError: Java heap space

排查步骤:

# 如果配置了 -XX:+HeapDumpOnOutOfMemoryError
# 会自动生成heap dump文件

# 1. 分析heap dump
# 使用MAT打开自动生成的hprof文件

# 2. 如果没有自动dump,手动生成
jmap -dump:format=b,file=oom.hprof <pid>

# 3. MAT分析步骤
# a. 打开Overview -> Leak Suspects
# b. 查看占用最多内存的对象
# c. 右键 -> Path to GC Roots -> exclude weak references
# d. 找到持有对象的引用链
# e. 分析为什么对象无法被回收

# 4. 常见原因
# - 集合类持续增长未清理
# - 缓存无过期策略
# - ThreadLocal未清理
# - 监听器未移除
# - 静态集合持有大量对象

第九章:监控脚本

9.1 一键诊断脚本

#!/bin/bash
# jvm_diagnosis.sh - JVM一键诊断脚本

PID=$1

if [ -z "$PID" ]; then
    echo "Usage: $0 <pid>"
    exit 1
fi

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="jvm_diagnosis_${PID}_${TIMESTAMP}"
mkdir -p $OUTPUT_DIR

echo "开始诊断 PID: $PID"
echo "输出目录: $OUTPUT_DIR"

# 1. 基本信息
echo "1. 收集基本信息..."
jps -lvm > $OUTPUT_DIR/jps.txt
jinfo -flags $PID > $OUTPUT_DIR/jinfo.txt

# 2. 线程信息
echo "2. 收集线程信息..."
jstack -l $PID > $OUTPUT_DIR/jstack.txt
jstack $PID | grep "java.lang.Thread.State" | sort | uniq -c > $OUTPUT_DIR/thread_state_summary.txt

# 3. GC信息
echo "3. 收集GC信息(30秒)..."
jstat -gcutil $PID 1000 30 > $OUTPUT_DIR/jstat_gcutil.txt

# 4. 堆信息
echo "4. 收集堆信息..."
jmap -heap $PID > $OUTPUT_DIR/jmap_heap.txt
jmap -histo $PID | head -100 > $OUTPUT_DIR/jmap_histo.txt

# 5. 检查死锁
echo "5. 检查死锁..."
grep -A 30 "Found.*deadlock" $OUTPUT_DIR/jstack.txt > $OUTPUT_DIR/deadlock.txt

# 6. 生成报告
echo "6. 生成诊断报告..."
cat > $OUTPUT_DIR/README.txt <<EOF
JVM诊断报告
==========
PID: $PID
时间: $TIMESTAMP

文件说明:
- jps.txt: Java进程信息
- jinfo.txt: JVM参数配置
- jstack.txt: 线程堆栈信息
- thread_state_summary.txt: 线程状态统计
- jstat_gcutil.txt: GC统计(30秒)
- jmap_heap.txt: 堆内存配置
- jmap_histo.txt: 对象分布Top100
- deadlock.txt: 死锁信息(如果有)

下一步:
1. 查看 thread_state_summary.txt 了解线程分布
2. 查看 deadlock.txt 检查是否有死锁
3. 查看 jstat_gcutil.txt 分析GC情况
4. 查看 jmap_histo.txt 分析内存使用

如需更详细分析,可以执行:
jmap -dump:live,format=b,file=heap.hprof $PID
EOF

echo "诊断完成!请查看目录: $OUTPUT_DIR"
ls -lh $OUTPUT_DIR/

9.2 综合监控脚本

#!/bin/bash
# comprehensive-monitor.sh

PID=$1
LOG_DIR="/var/log/jvm-monitor"
mkdir -p $LOG_DIR

while true; do
    TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
    echo "=== JVM 监控报告 - $TIMESTAMP ===" >> $LOG_DIR/jvm_monitor.log
    
    # 内存使用情况
    echo "--- 内存使用情况 ---" >> $LOG_DIR/jvm_monitor.log
    jstat -gc $PID 1s 1 >> $LOG_DIR/jvm_monitor.log
    
    # 线程状态
    echo "--- 线程状态 ---" >> $LOG_DIR/jvm_monitor.log
    jstack $PID | grep "java.lang.Thread.State" | sort | uniq -c >> $LOG_DIR/jvm_monitor.log
    
    # 类加载情况
    echo "--- 类加载情况 ---" >> $LOG_DIR/jvm_monitor.log
    jstat -class $PID 1s 1 >> $LOG_DIR/jvm_monitor.log
    
    echo "" >> $LOG_DIR/jvm_monitor.log
    sleep 60
done

9.3 告警脚本

#!/bin/bash
# alert-monitor.sh

PID=$1
THRESHOLD_CPU=80
THRESHOLD_MEMORY=85
THRESHOLD_GC=10

while true; do
    # CPU检查
    CPU_USAGE=$(ps -p $PID -o %cpu --no-headers | tr -d ' ')
    if (( $(echo "$CPU_USAGE > $THRESHOLD_CPU" | bc -l) )); then
        echo "$(date): CPU告警 - ${CPU_USAGE}%" | tee -a alert.log
    fi
    
    # 内存检查
    HEAP_USAGE=$(jstat -gc $PID | tail -1 | awk '{print ($3+$4+$6+$8)/($1+$2+$5+$7)*100}')
    if (( $(echo "$HEAP_USAGE > $THRESHOLD_MEMORY" | bc -l) )); then
        echo "$(date): 内存告警 - ${HEAP_USAGE}%" | tee -a alert.log
    fi
    
    # GC检查
    YGC_COUNT=$(jstat -gc $PID | tail -1 | awk '{print $13}')
    if [ $YGC_COUNT -gt $THRESHOLD_GC ]; then
        echo "$(date): GC告警 - ${YGC_COUNT}次" | tee -a alert.log
    fi
    
    sleep 30
done

第十章:命令速查表

10.1 进程管理命令

# 查看Java进程
jps -lvm

# 查看JVM参数
jinfo <pid>

# 查看特定JVM参数
jinfo -flag MaxHeapSize <pid>

# 动态修改JVM参数
jinfo -flag +PrintGC <pid>

10.2 内存监控命令

# GC监控
jstat -gc <pid> 1s 60
jstat -gcutil <pid> 1s 60
jstat -gccause <pid> 1s 60

# 内存分析
jmap -heap <pid>
jmap -histo <pid> | head -30
jmap -histo:live <pid> | head -30

# 堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>

10.3 线程监控命令

# 线程转储
jstack <pid> > threaddump.txt
jstack -l <pid> > threaddump.txt

# 死锁检测
jstack <pid> | grep -A 30 "deadlock"

# 线程状态统计
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c

10.4 综合诊断命令

# jcmd综合命令
jcmd <pid> help
jcmd <pid> Thread.print
jcmd <pid> GC.class_histogram
jcmd <pid> VM.flags
jcmd <pid> VM.system_properties

10.5 常用排查命令组合

快速诊断
# 获取进程信息
jps -l

# 查看JVM参数
jinfo <pid>

# 监控内存使用
jstat -gc <pid> 1s 5

# 查看线程状态
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c
深度诊断
# 生成堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>

# 生成线程转储
jstack <pid> > threaddump.txt

# 生成类直方图
jmap -histo:live <pid> > class_histogram.txt

# 查看GC信息
jstat -gc <pid> 1s 10 > gc_info.txt

10.6 常见问题速查

问题使用工具关键命令
CPU飙高jstacktop -H + jstack
内存泄漏jmapjmap -histo + dump
频繁GCjstatjstat -gcutil
死锁jstackjstack -l
OOMjmapjmap -heap + dump
响应慢jstackjstack(查看BLOCKED)
线程过多jstackjstack | grep “java.lang.Thread.State”

总结

JVM诊断工具使用原则

  1. 问题导向:根据问题类型选择合适的工具
  2. 组合使用:多个工具配合使用,全面分析
  3. 持续监控:建立监控体系,及时发现问题
  4. 数据驱动:基于监控数据做调优决策

工具选择建议

场景推荐工具原因
日常监控jconsole图形化界面,易于使用
问题排查jstack + jstat + jmap命令行工具,功能强大
深度分析MAT + JProfiler专业分析工具
自动化监控脚本 + 告警持续监控,及时响应

最佳实践

  1. 建立监控基线:记录正常情况下的指标
  2. 设置告警阈值:及时发现异常情况
  3. 定期健康检查:使用诊断脚本定期检查
  4. 问题记录分析:记录问题排查过程和解决方案
  5. 持续优化改进:根据监控数据持续优化

记住

  • jstack看线程(死锁、阻塞、CPU高)
  • jstat看GC(频率、耗时、使用率)
  • jmap看内存(配置、对象、泄漏)
  • jconsole看整体(图形化监控)

通过掌握这些诊断工具,可以有效地解决JVM线上问题,提高应用性能和稳定性!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值