
JVM 崩溃(Crash)是生产环境中最棘手的故障之一,它可能由多种原因引起:JNI 调用错误、JVM Bug、系统资源耗尽、硬件故障,甚至是 JDK 版本不兼容。本文将系统性地介绍 JVM 崩溃排查的完整方法论,涵盖每一步的操作细节和工具使用。
一、JVM 崩溃的常见类型
常见 JVM 运行时异常类型
- 内存溢出 OOM
Java heap space:堆内存溢出,对象过多、内存泄漏
Metaspace:元空间溢出,类、方法、动态代理过多
GC overhead limit exceeded:GC 频繁却回收极少,CPU 飙高 - 栈相关异常
StackOverflowError:递归死循环、方法调用层级过深
Unable to create new native thread:线程数超限、系统资源耗尽 - GC 异常
Full GC 频繁、GC 卡顿严重、STW 时间过长
GC 后内存不释放,内存持续走高 - 类加载 / 编译异常
NoClassDefFoundError、ClassNotFoundException
UnsatisfiedLinkError 本地库加载失败
1.1 崩溃信号分类
| 信号 | 含义 | 典型原因 |
|---|---|---|
| SIGSEGV (11) | 段错误/非法内存访问 | JNI 空指针、堆外内存越界、JVM Bug |
| SIGBUS (7) | 总线错误 | 内存对齐问题、硬件故障、文件映射损坏 |
| SIGILL (4) | 非法指令 | CPU 指令集不兼容、JVM 版本与架构不匹配 |
| SIGABRT (6) | 异常终止 | assert 失败、内存分配失败、GC 异常 |
| SIGFPE (8) | 浮点异常 | 除以零、数值溢出(极少见) |
1.2 崩溃日志位置
JVM 崩溃时会自动生成 hs_err_pid.log 文件,常见位置:
bash
# 默认位置(JVM 工作目录)
./hs_err_pid12345.log
# 通过参数指定位置
-XX:ErrorFile=/var/log/jvm/hs_err_pid%p.log
# 同时生成 Core Dump(Linux)
-XX:+CreateCoredumpOnCrash
ulimit -c unlimited # 确保系统允许生成 core 文件
二、排查第一步:收集崩溃现场
2.1 确认崩溃日志存在
bash
# 查找最近的崩溃日志
find / -name "hs_err_pid*.log" -mtime -1 2>/dev/null
# 查看崩溃时间
ls -la /path/to/hs_err_pid*.log
# 确认 core dump 文件
find / -name "core.*" -mtime -1 2>/dev/null
2.2 收集系统环境信息
bash
# 操作系统版本
cat /etc/os-release
uname -a
# JDK 版本(极其重要)
java -version
java -XshowSettings:all -version 2>&1 | head -20
# 系统资源状态
free -h
df -h
ulimit -a
# 当前运行的 JVM 进程
ps -ef | grep java
jps -lvm
2.3 保留现场的关键操作
bash
# 1. 立即备份崩溃日志
cp hs_err_pid12345.log /backup/crash-$(date +%Y%m%d-%H%M%S).log
# 2. 备份 core dump(如果存在且文件很大,可压缩)
cp core.12345 /backup/core-$(date +%Y%m%d-%H%M%S)
# 或
gzip -c core.12345 > /backup/core-$(date +%Y%m%d-%H%M%S).gz
# 3. 导出当前 JVM 的启动参数
jcmd <pid> VM.flags 2>/dev/null || cat /proc/<pid>/cmdline | tr '\0' ' '
# 4. 记录系统日志
journalctl --since "1 hour ago" > /backup/system-$(date +%Y%m%d-%H%M%S).log
三、排查第二步:分析 hs_err_pid 日志
3.1 日志结构总览
hs_err_pid 日志包含以下关键段落(按出现顺序):
1. 崩溃摘要(Crash Summary)
2. 线程信息(Thread Information)
3. 进程信息(Process Information)
4. 系统信息(System Information)
5. 内存映射(Memory Map)
6. VM 参数(VM Arguments)
7. 环境变量(Environment Variables)
8. 信号处理器(Signal Handlers)
3.2 关键段落详解
段落 1:崩溃摘要(定位问题类型)
text
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007f8b1c2a3f20, pid=12345, tid=0x00007f8b1b9fe700
#
# JRE version: OpenJDK Runtime Environment (17.0.8+7) (build 17.0.8+7-Ubuntu-1)
# Java VM: OpenJDK 64-Bit Server VM (17.0.8+7, mixed mode, sharing, tiered, compressed oops, g1 gc, linux-amd64)
# Problematic frame:
# C [libnative.so+0x3f20] Java_com_example_NativeBridge_processData+0x120
解读要点:
- SIGSEGV:段错误,非法内存访问
- pc=0x00007f8b1c2a3f20:程序计数器地址,崩溃发生的指令位置
- libnative.so+0x3f20 :崩溃发生在
libnative.so库的0x3f20偏移处 - Java_com_example_NativeBridge_processData:JNI 方法名,说明是 JNI 调用导致
段落 2:线程栈跟踪(定位代码位置)
text
--------------- T H R E A D ---------------
Current thread (0x00007f8b1800b800): JavaThread "http-worker-3" daemon [_thread_in_native, id=12350, stack(0x00007f8b1b8fe000,0x00007f8b1b9ff000)]
Stack: [0x00007f8b1b8fe000,0x00007f8b1b9ff000], sp=0x00007f8b1b9fd8a0, free space=1022k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C [libnative.so+0x3f20] Java_com_example_NativeBridge_processData+0x120
C [libnative.so+0x2a00] process_buffer+0x80
j com.example.NativeBridge.processData([B)I+0
j com.example.DataProcessor.processChunk(Ljava/nio/ByteBuffer;)V+15
j com.example.HttpWorker.run()V+89
v ~StubRoutines::call_stub
V [libjvm.so+0x8a3f20] JavaCalls::call_helper+0x3a0
关键信息:
- JavaThread "http-worker-3":崩溃线程名称
- _thread_in_native:线程正在执行 Native 代码(JNI)
- Native frames:Native 调用栈,从下往上是调用链
- j com.example.NativeBridge.processData:对应的 Java 方法
段落 3:寄存器状态(底层调试)
text
Registers:
RAX=0x0000000000000000, RBX=0x00007f8b1800b9d0, RCX=0x0000000000000064
RDX=0x0000000000000000, RSP=0x00007f8b1b9fd8a0, RBP=0x00007f8b1b9fd8c0
RSI=0x0000000000000000, RDI=0x00007f8b1c2a3f00, R8=0x0000000000000001
R9=0x0000000000000000, R10=0x0000000000000000, R11=0x0000000000000246
R12=0x00007f8b1800b800, R13=0x00007f8b1b9fd950, R14=0x00007f8b1b9fd960
R15=0x00007f8b1800b800, RIP=0x00007f8b1c2a3f20
注意:RAX=0 且是目标地址,说明访问了空指针。
段落 4:内存映射(定位库文件)
text
Memory map around native instruction:
[0x00007f8b1c2a3000-0x00007f8b1c2a4000] r-xp 00003000 08:01 1310724 /opt/app/lib/libnative.so
确认崩溃指令所在的内存页权限为 r-xp(可读可执行),说明是代码段。
四、排查第三步:使用专业工具深度分析
4.1 工具一:jcmd(JDK 自带,轻量诊断)
bash
# 列出所有 Java 进程
jcmd -l
# 查看指定进程的 VM 信息
jcmd <pid> VM.version
jcmd <pid> VM.flags # 查看 JVM 参数
jcmd <pid> VM.command_line # 查看启动命令
# 生成线程 Dump
jcmd <pid> Thread.print > thread_dump.txt
# 生成堆 Dump(崩溃前如果有机会执行)
jcmd <pid> GC.heap_dump /tmp/heap.hprof
# 查看 GC 统计
jcmd <pid> GC.run # 触发 GC
jcmd <pid> GC.class_histogram # 类实例统计
4.2 工具二:jstack(线程分析)
bash
# 打印所有线程栈
jstack -l <pid> > jstack.log
# 检测死锁
jstack -l <pid> | grep -A 50 "Found one Java-level deadlock"
# 打印混合栈(Java + Native)
jstack -m <pid> > jstack_mixed.log
4.3 工具三:jmap(内存分析)
bash
# 查看堆概要
jmap -heap <pid>
# 查看堆中对象的统计信息
jmap -histo <pid> | head -30
# 生成堆转储文件(线上慎用,会 STW)
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
# 查看 finalizer 队列(排查 Finalizer 导致的内存泄漏)
jmap -finalizerinfo <pid>
4.4 工具四:jinfo(运行时参数查看与修改)
bash
# 查看所有 VM 参数
jinfo -flags <pid>
# 查看特定参数值
jinfo -flag MaxHeapSize <pid>
# 动态修改参数(仅支持部分参数)
jinfo -flag +PrintGCDetails <pid>
jinfo -flag MinHeapFreeRatio=20 <pid>
4.5 工具五:jstat(GC 与内存监控)
bash
# 每 1 秒采样一次,共 10 次
jstat -gcutil <pid> 1000 10
# 输出示例:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 0.00 15.23 45.67 98.12 95.34 1234 45.6 12 8.9 54.5
# 各列含义:
# S0/S1: Survivor 区使用率
# E: Eden 区使用率
# O: 老年代使用率
# M: 元空间使用率
# YGC/YGCT: Young GC 次数/总时间
# FGC/FGCT: Full GC 次数/总时间
# 查看类加载统计
jstat -class <pid> 1000 5
4.6 工具六:jdb(Java 调试器)
bash
# 附加到运行中的进程
jdb -attach <pid>
# 常用命令
> threads # 列出所有线程
> thread <thread_id> # 切换到指定线程
> where # 打印当前线程栈
> print <expr> # 打印表达式值
> dump <object_id> # 打印对象详情
> stop at com.example.MyClass:42 # 设置断点
> cont # 继续执行
> exit # 退出
4.7 工具七:jhsdb(JDK 9+,服务性代理)
bash
# 分析 core dump 文件
jhsdb jstack --core core.12345 --exe /usr/lib/jvm/java-17/bin/java
# 分析堆(从 core dump 中提取)
jhsdb jmap --core core.12345 --exe /usr/lib/jvm/java-17/bin/java
# 交互式分析
jhsdb clhsdb --core core.12345 --exe /usr/lib/jvm/java-17/bin/java
4.8 工具八:GDB(分析 Core Dump)
bash
# 加载 core dump
gdb /usr/lib/jvm/java-17/bin/java core.12345
# 常用 GDB 命令
(gdb) bt # 打印完整调用栈
(gdb) bt full # 包含局部变量
(gdb) info registers # 查看寄存器
(gdb) info threads # 查看所有线程
(gdb) thread 5 # 切换到线程 5
(gdb) frame 3 # 切换到第 3 帧
(gdb) print *ptr # 打印指针内容
(gdb) x/16x $rsp # 查看栈内存
(gdb) disassemble # 反汇编当前函数
(gdb) info sharedlibrary # 查看加载的共享库
# 查找 JNI 调用
(gdb) info symbol 0x00007f8b1c2a3f20
(gdb) info line *0x00007f8b1c2a3f20
4.9 工具九:MAT(Memory Analyzer Tool)- 堆分析
bash
# 1. 下载 MAT:https://eclipse.dev/mat/
# 2. 打开堆转储文件
# 3. 运行 "Leak Suspects Report" 自动分析内存泄漏
# 4. 查看 "Dominator Tree" 找到大对象持有者
# 5. 查看 "Path to GC Roots" 找到引用链
4.10 工具十:Arthas(线上诊断神器)
bash
# 安装
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 常用命令
dashboard # 实时系统面板
thread # 线程信息
thread -n 10 # CPU 占用前 10 的线程
thread <tid> # 查看指定线程栈
jvm # JVM 信息
heapdump /tmp/dump.hprof # 生成堆转储
trace com.example.Service processData '#cost>100' # 方法耗时追踪
watch com.example.Service processData '{params,returnObj,throwExp}' # 方法入参出参监控
stack com.example.Service processData # 方法调用栈
tt -t com.example.Service processData # 方法执行时空隧道
五、排查第四步:常见崩溃场景与根因定位
5.1 场景一:JNI 调用导致的 SIGSEGV
日志特征:
text
# Problematic frame:
# C [libnative.so+0x3f20] Java_com_example_NativeBridge_processData+0x120
排查步骤:
bash
# 1. 确认 JNI 库文件
ldd /opt/app/lib/libnative.so
# 2. 检查库文件是否损坏
md5sum /opt/app/lib/libnative.so
file /opt/app/lib/libnative.so
# 3. 使用 nm/objdump 查看符号表
nm -D /opt/app/lib/libnative.so | grep processData
objdump -d /opt/app/lib/libnative.so | grep -A 20 "<Java_com_example_NativeBridge_processData>"
# 4. 检查 JNI 参数传递
# 在 Java 代码中添加 null 检查
# 确认数组越界保护
解决方案:
- 在 JNI 代码中添加空指针检查
- 使用
GetArrayLength验证数组边界 - 确保 Native 内存分配成功后再使用
5.2 场景二:元空间溢出导致的崩溃
日志特征:
text
# java.lang.OutOfMemoryError: Metaspace
# Internal exceptions (10 events):
# Event: 0.123 Thread 0x00007f8b1800b800 Exception <a 'java/lang/OutOfMemoryError'{0x00000000c0000000}: Metaspace> (0x00000000c0000000)
排查步骤:
bash
# 1. 查看类加载情况
jcmd <pid> VM.classloader_stats
# 2. 查看类加载器层次
jcmd <pid> VM.class_hierarchy
# 3. 使用 jstat 监控元空间
jstat -class <pid> 1000 10
# 4. 分析类加载日志(需开启 -XX:+TraceClassLoading)
解决方案:
bash
# 增大元空间
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
# 检查动态代理/字节码生成(如 CGLIB、Javassist)
# 检查 OSGi/热部署导致的类加载器泄漏
5.3 场景三:GC 导致的崩溃(GC Locker)
日志特征:
text
# A fatal error has been detected by the Java Runtime Environment:
# Internal Error (gcLocker.cpp:XXX), pid=12345, tid=XXX
# guarantee(_jni_lock_count >= count) failed: _jni_lock_count (0) < count (1)
排查步骤:
bash
# 1. 查看 GC 日志
cat /var/log/gc.log | grep -i "gc locker\|jni\|critical"
# 2. 检查 JNI Critical 区域使用
# 搜索代码中的 GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical
# 3. 使用 jstack 查看线程状态
jstack <pid> | grep -A 5 "JNI"
解决方案:
- 缩短 JNI Critical 区域持有时间
- 避免在 Critical 区域执行耗时操作
- 升级到修复了该问题的 JDK 版本
5.4 场景四:堆外内存(Direct Memory)溢出
日志特征:
text
# java.lang.OutOfMemoryError: Direct buffer memory
# 或 Native memory allocation (mmap) failed to allocate X bytes
排查步骤:
bash
# 1. 查看直接内存使用
jcmd <pid> VM.native_memory summary
# 2. 详细查看各区域
jcmd <pid> VM.native_memory detail
# 3. 开启 NMT(Native Memory Tracking)
-XX:NativeMemoryTracking=summary # 或 detail
# 然后使用:
jcmd <pid> VM.native_memory baseline # 建立基线
jcmd <pid> VM.native_memory summary.diff # 查看差异
# 4. 查看堆外内存分配
jcmd <pid> VM.native_memory detail | grep -A 5 "Internal"
解决方案:
bash
# 限制直接内存大小
-XX:MaxDirectMemorySize=1g
# 检查 Netty/Netty 等框架的 ByteBuf 泄漏
# 使用引用计数确保释放
5.5 场景五:JVM Bug / JDK 版本问题
日志特征:
text
# Internal Error (os_linux.cpp:XXX), pid=12345, tid=XXX
# Error: guarantee(requested_size <= size) failed
排查步骤:
bash
# 1. 确认 JDK 版本
java -version
# 2. 检查已知 Bug
# 访问 https://bugs.openjdk.org/
# 搜索错误信息中的文件名和行号
# 3. 检查 JDK 发行版
# Oracle JDK vs OpenJDK vs 各发行版(Adoptium、Amazon Corretto、Azul Zulu)
# 4. 检查操作系统兼容性
uname -a
cat /etc/os-release
解决方案:
- 升级到最新的补丁版本(如 17.0.8 → 17.0.12)
- 考虑更换 JDK 发行版
- 如果是已知 Bug,应用临时 workaround
5.6 场景六:系统资源耗尽
排查步骤:
bash
# 1. 文件描述符
lsof -p <pid> | wc -l
ulimit -n
cat /proc/<pid>/limits | grep "Max open files"
# 2. 内存
free -h
cat /proc/<pid>/status | grep -E "VmRSS|VmSize|VmPeak"
cat /proc/meminfo | grep -E "MemTotal|MemFree|Buffers|Cached"
# 3. 线程数
ps -eLf | grep <pid> | wc -l
ulimit -u
cat /proc/<pid>/status | grep Threads
# 4. 磁盘空间
df -h
六、排查第五步:建立预防机制
6.1 JVM 启动参数加固
bash
# ========== 崩溃现场保留 ==========
-XX:+CreateCoredumpOnCrash # 生成 core dump
-XX:ErrorFile=/var/log/jvm/hs_err_pid%p.log # 崩溃日志路径
-XX:HeapDumpPath=/var/log/jvm/ # OOM 堆转储路径
-XX:+HeapDumpOnOutOfMemoryError # OOM 时自动 dump
# ========== 内存监控 ==========
-XX:NativeMemoryTracking=summary # 开启 NMT
-XX:+PrintGCDetails # 打印 GC 详情
-XX:+PrintGCDateStamps # GC 时间戳
-Xlog:gc*:file=/var/log/jvm/gc.log:time,uptime,level,tags:filecount=10,filesize=100M # JDK 9+
# ========== 安全加固 ==========
-XX:+DisableExplicitGC # 禁止 System.gc()
-XX:+UseStringDeduplication # JDK 8u20+ 字符串去重
-XX:+OptimizeStringConcat # 字符串拼接优化
6.2 监控告警脚本
bash
#!/bin/bash
# jvm_crash_monitor.sh - 定时检查崩溃日志
LOG_DIR="/var/log/jvm"
ALERT_WEBHOOK="https://hooks.slack.com/services/XXX"
# 检查新崩溃
for log in $(find $LOG_DIR -name "hs_err_pid*.log" -mmin -5); do
PID=$(echo $log | grep -oP 'hs_err_pid\K\d+')
SIGNAL=$(grep -oP 'SIG\w+' $log | head -1)
THREAD=$(grep -oP 'Current thread.*' $log | head -1)
# 发送告警
curl -X POST $ALERT_WEBHOOK \
-H 'Content-Type: application/json' \
-d "{
\"text\": \"🚨 JVM Crash Detected\",
\"attachments\": [{
\"color\": \"danger\",
\"fields\": [
{\"title\": \"PID\", \"value\": \"$PID\", \"short\": true},
{\"title\": \"Signal\", \"value\": \"$SIGNAL\", \"short\": true},
{\"title\": \"Thread\", \"value\": \"$THREAD\", \"short\": false},
{\"title\": \"Log\", \"value\": \"$log\", \"short\": false}
]
}]
}"
# 自动收集现场
/opt/scripts/collect_crash_info.sh $PID $log
done
6.3 崩溃信息收集脚本
bash
#!/bin/bash
# collect_crash_info.sh
PID=$1
LOG_FILE=$2
OUTPUT_DIR="/backup/crash/$(date +%Y%m%d-%H%M%S)-$PID"
mkdir -p $OUTPUT_DIR
# 复制崩溃日志
cp $LOG_FILE $OUTPUT_DIR/
# 收集系统信息
uname -a > $OUTPUT_DIR/system_info.txt
cat /proc/cpuinfo > $OUTPUT_DIR/cpuinfo.txt
free -h > $OUTPUT_DIR/memory.txt
df -h > $OUTPUT_DIR/disk.txt
cat /proc/$PID/status > $OUTPUT_DIR/process_status.txt 2>/dev/null
cat /proc/$PID/limits > $OUTPUT_DIR/process_limits.txt 2>/dev/null
cat /proc/$PID/maps > $OUTPUT_DIR/memory_map.txt 2>/dev/null
# 收集 JVM 信息
jcmd $PID VM.version > $OUTPUT_DIR/jvm_version.txt 2>/dev/null
jcmd $PID VM.flags > $OUTPUT_DIR/jvm_flags.txt 2>/dev/null
jcmd $PID VM.command_line > $OUTPUT_DIR/jvm_cmdline.txt 2>/dev/null
# 收集线程和堆信息(如果进程还在)
jstack -l $PID > $OUTPUT_DIR/thread_dump.txt 2>/dev/null
jmap -heap $PID > $OUTPUT_DIR/heap_info.txt 2>/dev/null
# 打包
tar czf $OUTPUT_DIR.tar.gz $OUTPUT_DIR
echo "Crash info collected: $OUTPUT_DIR.tar.gz"
七、完整排查流程图
┌─────────────────────────────────────────────────────────────┐
│ JVM 崩溃发生 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 1: 收集现场 │
│ ├── 查找 hs_err_pid*.log │
│ ├── 检查 core dump 文件 │
│ ├── 记录系统状态(free, df, ulimit) │
│ └── 备份所有相关文件 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 2: 分析崩溃日志 │
│ ├── 查看崩溃信号(SIGSEGV/SIGBUS/SIGILL) │
│ ├── 定位 Problematic frame │
│ ├── 分析线程栈(Java + Native) │
│ └── 检查寄存器和内存映射 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 3: 确定崩溃类型 │
│ ├── JNI 调用错误? → 检查 Native 代码 │
│ ├── 内存溢出? → 分析堆/元空间/直接内存 │
│ ├── GC 异常? → 检查 GC 日志和参数 │
│ ├── JVM Bug? → 查询 OpenJDK Bug 库 │
│ └── 系统资源? → 检查 fd/内存/线程/磁盘 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 4: 深度分析(使用工具) │
│ ├── jcmd / jstack / jmap → 运行时信息 │
│ ├── jhsdb → 分析 core dump │
│ ├── GDB → 底层调试 │
│ ├── MAT → 堆内存分析 │
│ └── Arthas → 线上诊断 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 5: 根因定位与修复 │
│ ├── 修复代码(JNI 空指针、内存泄漏) │
│ ├── 调整 JVM 参数(堆大小、GC 策略) │
│ ├── 升级 JDK 版本 │
│ └── 扩容系统资源 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 6: 建立预防机制 │
│ ├── 完善 JVM 启动参数 │
│ ├── 部署监控告警脚本 │
│ ├── 定期健康检查 │
│ └── 压测验证修复效果 │
└─────────────────────────────────────────────────────────────┘
八、总结
JVM 崩溃排查的核心在于快速收集现场、精准定位根因、系统性修复预防。记住以下要点:
- 崩溃日志是黄金 :
hs_err_pid日志包含了 90% 的诊断信息 - core dump 是保险 :开启
-XX:+CreateCoredumpOnCrash,配合 GDB/jhsdb 深度分析 - 工具链要熟练:jcmd/jstack/jmap 用于运行时,MAT 用于堆分析,Arthas 用于线上诊断
- 预防胜于治疗:完善的参数配置 + 监控告警 + 定期演练,才能将崩溃风险降到最低