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文件,常见位置:
# 默认位置(JVM 工作目录)./hs_err_pid12345.log# 通过参数指定位置-XX:ErrorFile=/var/log/jvm/hs_err_pid%p.log# 同时生成 Core Dump(Linux)-XX:+CreateCoredumpOnCrashulimit-cunlimited# 确保系统允许生成 core 文件二、排查第一步:收集崩溃现场
2.1 确认崩溃日志存在
# 查找最近的崩溃日志find/-name"hs_err_pid*.log"-mtime-12>/dev/null# 查看崩溃时间ls-la/path/to/hs_err_pid*.log# 确认 core dump 文件find/-name"core.*"-mtime-12>/dev/null2.2 收集系统环境信息
# 操作系统版本cat/etc/os-releaseuname-a# JDK 版本(极其重要)java-versionjava-XshowSettings:all-version2>&1|head-20# 系统资源状态free-hdf-hulimit-a# 当前运行的 JVM 进程ps-ef|grepjavajps-lvm2.3 保留现场的关键操作
# 1. 立即备份崩溃日志cphs_err_pid12345.log /backup/crash-$(date+%Y%m%d-%H%M%S).log# 2. 备份 core dump(如果存在且文件很大,可压缩)cpcore.12345 /backup/core-$(date+%Y%m%d-%H%M%S)# 或gzip-ccore.12345>/backup/core-$(date+%Y%m%d-%H%M%S).gz# 3. 导出当前 JVM 的启动参数jcmd<pid>VM.flags2>/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:崩溃摘要(定位问题类型)
# # 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:线程栈跟踪(定位代码位置)
--------------- 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:寄存器状态(底层调试)
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:内存映射(定位库文件)
Memory map around native instruction: [0x00007f8b1c2a3000-0x00007f8b1c2a4000] r-xp 00003000 08:01 1310724 /opt/app/lib/libnative.so确认崩溃指令所在的内存页权限为r-xp(可读可执行),说明是代码段。
四、排查第三步:使用专业工具深度分析
4.1 工具一:jcmd(JDK 自带,轻量诊断)
# 列出所有 Java 进程jcmd-l# 查看指定进程的 VM 信息jcmd<pid>VM.version jcmd<pid>VM.flags# 查看 JVM 参数jcmd<pid>VM.command_line# 查看启动命令# 生成线程 Dumpjcmd<pid>Thread.print>thread_dump.txt# 生成堆 Dump(崩溃前如果有机会执行)jcmd<pid>GC.heap_dump /tmp/heap.hprof# 查看 GC 统计jcmd<pid>GC.run# 触发 GCjcmd<pid>GC.class_histogram# 类实例统计4.2 工具二:jstack(线程分析)
# 打印所有线程栈jstack-l<pid>>jstack.log# 检测死锁jstack-l<pid>|grep-A50"Found one Java-level deadlock"# 打印混合栈(Java + Native)jstack-m<pid>>jstack_mixed.log4.3 工具三:jmap(内存分析)
# 查看堆概要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(运行时参数查看与修改)
# 查看所有 VM 参数jinfo-flags<pid># 查看特定参数值jinfo-flagMaxHeapSize<pid># 动态修改参数(仅支持部分参数)jinfo-flag+PrintGCDetails<pid>jinfo-flagMinHeapFreeRatio=20<pid>4.5 工具五:jstat(GC 与内存监控)
# 每 1 秒采样一次,共 10 次jstat-gcutil<pid>100010# 输出示例:# 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>100054.6 工具六:jdb(Java 调试器)
# 附加到运行中的进程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+,服务性代理)
# 分析 core dump 文件jhsdb jstack--corecore.12345--exe/usr/lib/jvm/java-17/bin/java# 分析堆(从 core dump 中提取)jhsdb jmap--corecore.12345--exe/usr/lib/jvm/java-17/bin/java# 交互式分析jhsdb clhsdb--corecore.12345--exe/usr/lib/jvm/java-17/bin/java4.8 工具八:GDB(分析 Core Dump)
# 加载 core dumpgdb /usr/lib/jvm/java-17/bin/java core.12345# 常用 GDB 命令(gdb)bt# 打印完整调用栈(gdb)bt full# 包含局部变量(gdb)info registers# 查看寄存器(gdb)info threads# 查看所有线程(gdb)thread5# 切换到线程 5(gdb)frame3# 切换到第 3 帧(gdb)print *ptr# 打印指针内容(gdb)x/16x$rsp# 查看栈内存(gdb)disassemble# 反汇编当前函数(gdb)info sharedlibrary# 查看加载的共享库# 查找 JNI 调用(gdb)info symbol 0x00007f8b1c2a3f20(gdb)info line *0x00007f8b1c2a3f204.9 工具九:MAT(Memory Analyzer Tool)- 堆分析
# 1. 下载 MAT:https://eclipse.dev/mat/# 2. 打开堆转储文件# 3. 运行 "Leak Suspects Report" 自动分析内存泄漏# 4. 查看 "Dominator Tree" 找到大对象持有者# 5. 查看 "Path to GC Roots" 找到引用链4.10 工具十:Arthas(线上诊断神器)
# 安装curl-Ohttps://arthas.aliyun.com/arthas-boot.jarjava-jararthas-boot.jar# 常用命令dashboard# 实时系统面板thread# 线程信息thread-n10# CPU 占用前 10 的线程thread<tid># 查看指定线程栈jvm# JVM 信息heapdump /tmp/dump.hprof# 生成堆转储trace com.example.Service processData'#cost>100'# 方法耗时追踪watchcom.example.Service processData'{params,returnObj,throwExp}'# 方法入参出参监控stack com.example.Service processData# 方法调用栈tt-tcom.example.Service processData# 方法执行时空隧道五、排查第四步:常见崩溃场景与根因定位
5.1 场景一:JNI 调用导致的 SIGSEGV
日志特征:
# Problematic frame: # C [libnative.so+0x3f20] Java_com_example_NativeBridge_processData+0x120排查步骤:
# 1. 确认 JNI 库文件ldd /opt/app/lib/libnative.so# 2. 检查库文件是否损坏md5sum /opt/app/lib/libnative.sofile/opt/app/lib/libnative.so# 3. 使用 nm/objdump 查看符号表nm-D/opt/app/lib/libnative.so|grepprocessData objdump-d/opt/app/lib/libnative.so|grep-A20"<Java_com_example_NativeBridge_processData>"# 4. 检查 JNI 参数传递# 在 Java 代码中添加 null 检查# 确认数组越界保护解决方案:
- 在 JNI 代码中添加空指针检查
- 使用
GetArrayLength验证数组边界 - 确保 Native 内存分配成功后再使用
5.2 场景二:元空间溢出导致的崩溃
日志特征:
# java.lang.OutOfMemoryError: Metaspace # Internal exceptions (10 events): # Event: 0.123 Thread 0x00007f8b1800b800 Exception <a 'java/lang/OutOfMemoryError'{0x00000000c0000000}: Metaspace> (0x00000000c0000000)排查步骤:
# 1. 查看类加载情况jcmd<pid>VM.classloader_stats# 2. 查看类加载器层次jcmd<pid>VM.class_hierarchy# 3. 使用 jstat 监控元空间jstat-class<pid>100010# 4. 分析类加载日志(需开启 -XX:+TraceClassLoading)解决方案:
# 增大元空间-XX:MetaspaceSize=256m-XX:MaxMetaspaceSize=512m# 检查动态代理/字节码生成(如 CGLIB、Javassist)# 检查 OSGi/热部署导致的类加载器泄漏5.3 场景三:GC 导致的崩溃(GC Locker)
日志特征:
# 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)排查步骤:
# 1. 查看 GC 日志cat/var/log/gc.log|grep-i"gc locker\|jni\|critical"# 2. 检查 JNI Critical 区域使用# 搜索代码中的 GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical# 3. 使用 jstack 查看线程状态jstack<pid>|grep-A5"JNI"解决方案:
- 缩短 JNI Critical 区域持有时间
- 避免在 Critical 区域执行耗时操作
- 升级到修复了该问题的 JDK 版本
5.4 场景四:堆外内存(Direct Memory)溢出
日志特征:
# java.lang.OutOfMemoryError: Direct buffer memory # 或 Native memory allocation (mmap) failed to allocate X bytes排查步骤:
# 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-A5"Internal"解决方案:
# 限制直接内存大小-XX:MaxDirectMemorySize=1g# 检查 Netty/Netty 等框架的 ByteBuf 泄漏# 使用引用计数确保释放5.5 场景五:JVM Bug / JDK 版本问题
日志特征:
# Internal Error (os_linux.cpp:XXX), pid=12345, tid=XXX # Error: guarantee(requested_size <= size) failed排查步骤:
# 1. 确认 JDK 版本java-version# 2. 检查已知 Bug# 访问 https://bugs.openjdk.org/# 搜索错误信息中的文件名和行号# 3. 检查 JDK 发行版# Oracle JDK vs OpenJDK vs 各发行版(Adoptium、Amazon Corretto、Azul Zulu)# 4. 检查操作系统兼容性uname-acat/etc/os-release解决方案:
- 升级到最新的补丁版本(如 17.0.8 → 17.0.12)
- 考虑更换 JDK 发行版
- 如果是已知 Bug,应用临时 workaround
5.6 场景六:系统资源耗尽
排查步骤:
# 1. 文件描述符lsof-p<pid>|wc-lulimit-ncat/proc/<pid>/limits|grep"Max open files"# 2. 内存free-hcat/proc/<pid>/status|grep-E"VmRSS|VmSize|VmPeak"cat/proc/meminfo|grep-E"MemTotal|MemFree|Buffers|Cached"# 3. 线程数ps-eLf|grep<pid>|wc-lulimit-ucat/proc/<pid>/status|grepThreads# 4. 磁盘空间df-h六、排查第五步:建立预防机制
6.1 JVM 启动参数加固
# ========== 崩溃现场保留 ==========-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 监控告警脚本
#!/bin/bash# jvm_crash_monitor.sh - 定时检查崩溃日志LOG_DIR="/var/log/jvm"ALERT_WEBHOOK="https://hooks.slack.com/services/XXX"# 检查新崩溃forlogin$(find$LOG_DIR-name"hs_err_pid*.log"-mmin-5);doPID=$(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-XPOST$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$logdone6.3 崩溃信息收集脚本
#!/bin/bash# collect_crash_info.shPID=$1LOG_FILE=$2OUTPUT_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.txtcat/proc/cpuinfo>$OUTPUT_DIR/cpuinfo.txtfree-h>$OUTPUT_DIR/memory.txtdf-h>$OUTPUT_DIR/disk.txtcat/proc/$PID/status>$OUTPUT_DIR/process_status.txt2>/dev/nullcat/proc/$PID/limits>$OUTPUT_DIR/process_limits.txt2>/dev/nullcat/proc/$PID/maps>$OUTPUT_DIR/memory_map.txt2>/dev/null# 收集 JVM 信息jcmd$PIDVM.version>$OUTPUT_DIR/jvm_version.txt2>/dev/null jcmd$PIDVM.flags>$OUTPUT_DIR/jvm_flags.txt2>/dev/null jcmd$PIDVM.command_line>$OUTPUT_DIR/jvm_cmdline.txt2>/dev/null# 收集线程和堆信息(如果进程还在)jstack-l$PID>$OUTPUT_DIR/thread_dump.txt2>/dev/null jmap-heap$PID>$OUTPUT_DIR/heap_info.txt2>/dev/null# 打包tarczf$OUTPUT_DIR.tar.gz$OUTPUT_DIRecho"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 用于线上诊断
- 预防胜于治疗:完善的参数配置 + 监控告警 + 定期演练,才能将崩溃风险降到最低