HOME

Java 应用反复被杀:从 OOM 排查到 JRE 1.8 SIGSEGV 的定位记录

文章目录12 节

这次现场遇到的现象很普通:一个 Java 应用运行一段时间后会消失,最开始只能把问题描述成“应用一直被杀掉”。

但最后的结论不是 Linux OOM Killer,也不是人为 kill -9,而是某个业务流程在 JRE 1.8 运行时路径下触发 JVM/native 层崩溃,Java 进程发生 SIGSEGV 段错误,并被 systemd-coredump 记录为 core dump。后续切换到 JDK 11 后,触发问题的业务流程稳定通过,应用不再异常退出。

这类问题最容易走偏。Java 进程没了,第一反应往往是查内存、查 OOM、查脚本杀进程。内存当然要查,但不能只盯内存。我这次的思路是先把“被杀”拆成几类证据,再逐层排除。

现场背景

应用部署在 /data/BES9.5,通过 BES 管理工具启动:

cd /data/BES9.5/bin
./iastool --user admin --password '******' --passport '******' start --server

现场截图里能看到 Java 进程类似:

root 1112619 1 ... /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.../jre/bin/java ...

几个信息值得先记下来:

  • Java PID 是 1112619
  • PPID=1,说明 iastool 启动完服务后已经退出,Java 进程挂到了 init/systemd 下面。
  • 运行路径是 .../jre/bin/java,现场旧运行时准确说是 JRE 1.8 / Java 8 运行时,不是完整 JDK 8。
  • JVM 参数里有 -Xms512m-Xmx1024m-XX:MaxMetaspaceSize=1024m
  • 现场有 /data/BES9.5/logs/server.log/data/BES9.5/logs/jvm.log、历史 .hprof 和历史 hs_err_pid*.log

这里还有一个安全细节:启动命令中出现过明文口令。整理记录时我统一做了脱敏,生产环境也建议改用受控脚本、环境变量、凭据文件或交互方式,避免密码长期留在 shell history、截图和工单系统里。

先把“被杀”分成几种

Java 进程突然没了,不要直接等同于 OOM。常见情况至少有下面几类:

现象常见证据排查入口
Linux OOM KillerKilled process ... (java)Out of memorydmesgjournalctl -k
容器内存限制OOMKilled、退出码 137docker inspectkubectl describe pod
Java 堆或堆外 OOMOutOfMemoryError.hprof应用日志、heap dump、jcmd
外部停止或重启SIGTERMSIGKILL、管理工具 stop/restartjournalctl、cron、history、auditd
资源限制Too many open files、线程创建失败、cgroup 限制/proc/<pid>/limitsulimit、systemd/cgroup
JVM 或 native 崩溃SIGSEGVSIGBUSSIGABRT、core dump、hs_err_pidcoredumpctlhs_err_pidgdb

这张表的价值在于避免先入为主。不同原因会留下不同证据,不能只靠“应用没了”做结论。

第一步:排除 Linux OOM Killer

最先查内核日志:

dmesg -T | grep -iE "killed process|out of memory|oom|java" | tail -n 100

如果系统启用了 journal,再查:

journalctl -k | grep -iE "killed process|out of memory|oom|java" | tail -n 100

如果看到类似下面的内容,才可以把方向定到系统 OOM:

Out of memory: Killed process 12345 (java)
Killed process 12345 (java), UID 0, total-vm...

本次现场执行后没有输出。这个结果不能 100% 证明没有发生过 OOM,因为 dmesg 缓冲可能被覆盖,系统也可能没有持久化内核日志。但至少在当前证据里,不能把 Linux OOM Killer 当成结论。

同时看当前资源状态:

free -h
swapon --show
ps -p 1112619 -o pid,ppid,user,etime,%cpu,%mem,rss,vsz,cmd
cat /proc/1112619/status | grep -E "VmRSS|VmSize|VmPeak|Threads"

这里要特别注意 Java 的总内存不等于 -Xmx。即使 -Xmx1024m,进程 RSS 也可能明显超过 1 GB,因为还包括 Metaspace、Direct Memory、线程栈、Code Cache、JVM native 内存、JNI 和 mmap 文件等。

第二步:不要把 JIT 编译日志误判成 OOM

现场曾经查到 /data/BES9.5/logs/jvm.log,里面出现了类似:

<dependency_failed ...>

并且能看到一些类名,例如:

java/lang/OutOfMemoryError
io/netty/util/internal/OutOfDirectMemoryError

这里很容易误判。jvm.log 里的 <dependency_failed> 更像 HotSpot 的 JIT/编译相关日志,不是业务异常栈,也不是 GC 日志本身。它提到了 OutOfMemoryError 这个类名,不代表应用已经真的抛出了 OOM。

更可靠的做法是分开查业务日志和 JVM 日志:

grep -RniE "OutOfMemoryError|OutOfDirectMemoryError|Direct buffer memory|Java heap space|Metaspace|unable to create new native thread" \
  /data/BES9.5/logs/server*.log | tail -n 300

grep -niE "OutOfMemory|OutOfDirectMemory|Direct buffer|Full GC|GC overhead|SIG|fatal" \
  /data/BES9.5/logs/jvm.log | grep -v "dependency_failed" | tail -n 200

再看有没有本次时间点生成的 heap dump:

find /data/BES9.5/logs -name "*.hprof" -ls
find /data/BES9.5/logs -name "*.hprof" -newermt "2026-06-16 12:00:00" -ls

本次现场看到的 .hprof 主要是旧文件,时间对不上,所以不能拿旧 dump 解释这次重启。

第三步:排查外部停止和资源限制

因为 Java 是通过 iastool 启动的,必须查 BES 管理工具、systemd、定时任务和人工操作。

先确认 Java 当前启动时间:

pid=$(pgrep -f "/data/BES9.5" | head -n 1)
echo "$pid"
ps -p "$pid" -o pid,ppid,user,etime,lstart,%cpu,%mem,rss,vsz,cmd

围绕启动时间查系统日志:

journalctl --since "2026-06-16 12:00:00" --until "2026-06-16 12:06:00" \
  | grep -iE "java|BES|iastool|kill|killed|terminated|oom|out of memory|stop|start|restart|shutdown|signal|exited"

查是否有 BES 相关服务:

systemctl list-units --type=service | grep -iE "bes|java"
grep -RniE "BES9.5|iastool|java" /etc/systemd/system /usr/lib/systemd/system 2>/dev/null

查定时任务和历史命令:

crontab -l
grep -RniE "BES9.5|iastool|java|kill|stop|restart|shutdown" /etc/cron* /var/spool/cron 2>/dev/null
history | grep -iE "iastool|start|stop|restart|kill|java|BES9.5|shutdown|reboot" | tail -n 100

资源限制也要查,尤其是文件句柄、线程数、虚拟内存和 cgroup 限制:

cat /proc/$pid/limits
cat /proc/$pid/status | grep -E "VmRSS|VmSize|VmPeak|Threads|FDSize|State"
ls /proc/$pid/fd | wc -l
ps -Lf -p "$pid" | wc -l
df -h
df -i
cat /proc/$pid/cgroup

如果是 systemd 管理的服务,再看:

systemctl show <service-name> | grep -iE "Limit|Memory|Tasks|CPU|OOM"

如果怀疑有人或脚本发了 kill,最有效的是提前开审计:

systemctl status auditd
auditctl -a always,exit -F arch=b64 -S kill -S tkill -S tgkill -k kill_bes
auditctl -a always,exit -F arch=b32 -S kill -S tkill -S tgkill -k kill_bes
ausearch -k kill_bes -i

本次中间阶段确实看到过 root 登录、SFTP 访问日志目录等线索,但这些只能提示“外部操作值得查”,不能直接证明是人工 kill。最终定性的证据来自 core dump。

关键转折:systemd-coredump 抓到了 Java core dump

后续日志里出现了关键行:

systemd-coredump[1115452]: Process 1112619 (java) of user 0 dumped core.

再看 coredumpctl info,核心信息是:

Signal: 11 (SEGV)
Process 1112619 (java) of user 0 dumped core.

这时排查方向就变了。

Signal: 11 (SEGV) 表示 Java 进程发生了段错误。它不是普通 Java 异常,也不是典型的 Linux OOM Killer,更不像单纯的 kill -9。它说明进程在 native 层崩溃了,可能落在 JVM 自身、JIT 编译器、JNI/native 库、系统库或某个触发 native 路径的第三方组件上。

用命令复核:

coredumpctl list | grep java
coredumpctl info 1112619
coredumpctl info --since "2026-06-16 12:00:00" --until "2026-06-16 12:03:00"

如果需要导出 core:

coredumpctl dump 1112619 -o /tmp/core.java.1112619
ls -lh /tmp/core.java.1112619
file /tmp/core.java.1112619

如果机器上有 gdb,可以继续看 native 栈:

coredumpctl debug 1112619

进入 gdb 后执行:

bt
thread apply all bt
quit

这一步的目标不是立刻修复,而是确认崩溃落点。如果栈里能看到 libjvm.solibfontmanager.solibawt.solibzip.so、第三方 libxxx.so,下一步策略会完全不同。

还要找 hs_err_pid

Java fatal error 通常会生成 hs_err_pid*.log。这个文件比 core dump 更适合一线运维快速判断。

按时间找:

find /data/BES9.5 /tmp /var/tmp /root -name "hs_err_pid*.log" -newermt "2026-06-16 12:00:00" -ls

按 PID 找:

find / -name "hs_err_pid1112619.log" 2>/dev/null

如果找到,先看前 120 行:

head -n 120 /path/to/hs_err_pid1112619.log

重点看:

# A fatal error has been detected by the Java Runtime Environment
# SIGSEGV
# Problematic frame:
Current thread:
Native frames:
Java frames:
Command Line:
VM Arguments:

其中 Problematic frame 最关键:

# Problematic frame:
# V  [libjvm.so+...]

可能指向 JVM/JIT/GC 方向。

# Problematic frame:
# C  [libxxx.so+...]

通常要继续查这个 native 库来自哪个包、哪个业务组件或哪个系统库。

如果现场没有生成新的 hs_err_pid,建议把错误日志路径显式加到 JVM 参数里:

-XX:ErrorFile=/data/BES9.5/logs/hs_err_pid%p.log

这样下次 fatal error 不会散落在启动目录、/tmp 或其他不确定位置。

本次修复:切换到 JDK 11

现场最终处理是:把运行时从 JRE 1.8 切换到 JDK 11。更换后,触发问题的业务流程不再导致 Java 进程崩溃。

到这里,结论边界也要收住:这次能确认的是 JRE 1.8 运行时路径下,某个业务流程触发了 JVM/native 层 SIGSEGV;切换到 JDK 11 后,业务流程稳定通过,故障不再复现

这次复盘只把根因写到这个层级:JRE 1.8 运行时缺陷或兼容性问题。现场没有补齐新的 hs_err_pidProblematic frame、gdb 完整栈或厂商确认,就不再绑定具体公开 JDK Bug 编号。生产复盘要把恢复动作写明确,未验证的根因不要写满。

升级验证建议按下面顺序做:

java -version
readlink -f "$(which java)"

pid=$(pgrep -f "/data/BES9.5" | head -n 1)
cat /proc/$pid/cmdline | tr '\0' ' ' | sed 's/ -/\n-/g' | head -n 80

确认 BES 实际使用的是 JDK 11,而不是系统默认 java 已切换但服务仍然引用旧路径。

然后复跑触发问题的业务流程,观察:

journalctl --since "30 minutes ago" | grep -iE "java|coredump|segfault|SIGSEGV|dumped core|BES"
coredumpctl list | grep java
tail -n 200 /data/BES9.5/logs/server.log

如果连续复跑业务流程后没有新的 SIGSEGV、没有新的 core dump、应用日志也没有异常退出,才能认为升级有效。

一套可复用的排查顺序

以后遇到“Java 应用被杀”,可以按这个顺序走。

第一组,先定进程和时间:

date
ps -ef | grep java | grep -v grep
pid=$(pgrep -f "/data/BES9.5" | head -n 1)
echo "$pid"
ps -p "$pid" -o pid,ppid,user,etime,lstart,%cpu,%mem,rss,vsz,cmd

第二组,查 OOM 和系统终止:

dmesg -T | grep -iE "killed process|out of memory|oom|java" | tail -n 100
journalctl -k | grep -iE "killed process|out of memory|oom|java" | tail -n 100
journalctl --since "2 hours ago" | grep -iE "java|BES|kill|killed|terminated|oom|out of memory|signal|exited|coredump"

第三组,查应用日志和 Java OOM:

grep -RniE "OutOfMemoryError|OutOfDirectMemoryError|Direct buffer memory|Java heap space|Metaspace|unable to create new native thread|Exception|Error|fatal|shutdown|stopping|stopped" \
  /data/BES9.5/logs/server*.log | tail -n 300

find /data/BES9.5/logs -name "*.hprof" -ls
find /data/BES9.5 /tmp /var/tmp /root -name "hs_err_pid*.log" -ls

第四组,查资源限制:

cat /proc/$pid/limits
cat /proc/$pid/status | grep -E "VmRSS|VmSize|VmPeak|Threads|FDSize|State"
ls /proc/$pid/fd | wc -l
ps -Lf -p "$pid" | wc -l
free -h
swapon --show
df -h
df -i

第五组,查 core dump:

coredumpctl list | grep java
coredumpctl info "$pid"
coredumpctl info --since "2 hours ago" | less

第六组,查外部管理动作:

systemctl list-units --type=service | grep -iE "bes|java"
grep -RniE "BES9.5|iastool|java" /etc/systemd/system /usr/lib/systemd/system 2>/dev/null
crontab -l
grep -RniE "BES9.5|iastool|java|kill|stop|restart|shutdown" /etc/cron* /var/spool/cron 2>/dev/null
history | grep -iE "iastool|start|stop|restart|kill|java|BES9.5|shutdown|reboot" | tail -n 100

判断树

可以按下面的顺序判断:

Java 进程没了
  |
  |-- dmesg/journalctl -k 有 Killed process java / Out of memory
  |     -> Linux OOM Killer
  |
  |-- 容器状态有 OOMKilled / memory limit
  |     -> 容器内存限制
  |
  |-- server.log 有 OutOfMemoryError,且有新 .hprof
  |     -> Java 堆或堆外 OOM
  |
  |-- journalctl 有 stop/restart/SIGTERM/SIGKILL,或 auditd 抓到 kill
  |     -> 外部停止、脚本、管理平台、人工操作
  |
  |-- /proc limits、systemd show、cgroup 有限制命中
  |     -> 文件句柄、线程数、内存、pids 或磁盘资源限制
  |
  |-- coredumpctl 有 java dumped core,Signal: 11/6/7
        -> JVM/native crash,继续看 hs_err_pid 和 gdb 栈

本次就落在最后一类:systemd-coredump 记录了 Java core dump,信号是 SIGSEGV

复盘

这次排查最有价值的地方,不是最后换了 JDK 11,而是中间没有过早下结论。

dmesg 没有 OOM 记录时,没有继续强行解释成 OOM;jvm.log 里出现 OutOfMemoryError 类名时,也没有把 JIT 编译日志当成业务异常;看到 root 登录时,也没有直接判定是人为重启。直到 systemd-coredump 给出 Process ... java dumped coreSignal: 11 (SEGV),排查方向才真正从“谁杀了进程”转成“JVM/native 为什么崩溃”。

最终通过 JDK 11 规避了 JRE 1.8 路径下的崩溃路径。后续如果还要做更细的根因确认,应补齐 hs_err_pidProblematic frame 和 gdb 回溯;如果只是生产恢复,JDK 11 的稳定性验证已经足够支撑本次处置。

参考与引用

Java Linux JRE JDK 故障排查 systemd-coredump