之前在写另外一篇文章的时候, 讨论到如果遇到 fatal error
, 就能通过 JVM 启动参数 -XX:OnError
来触发执行定制的命令, 以及用 -XX:ErrorFile
来产生 hs_err_pid*.log
. 那么什么是 fatal error
呢?
我们知道Java 的错误分为 Error
和 Exception
可是即便抛出一个 Error
并不能触发上面的2个参数, 所以文档里指的 fatal error
肯定是更为严重的错误.
你以前可能看到过 hs_err_pid*.log
, 可是真要故意创建一个 fatal error
的时候, 却犯难了.
如何创建一个 fatal error
有趣的是, 但我问 chatGPT 如何创建这种 fatal error
场景的时候, 它提示我: This content may violate our usage policies.
竟然拒绝回答. 当我告诉它, 我只是在做一个本地实验的时候, 它告诉我通过下面3种方法能造成 fatal error
:
- 调用 native C 代码, 读取错误地址的代码, 造成 Segment fault;
- 错误使用 JVM 参数, 如设置
-XX:CompileThreshold
特小, 导致编译死循环; - 手动触发特定 Error, 如
OutOfMemoryError
.
试了上面的第三项, 不论是通过消耗内存真造成 OOM 还是直接抛出 OutOfMemoryError
都无法触发.
谷歌搜索了一下, 也没找到我这种想触发的人, 都是遇到这种问题的人.
思路 从JDK 源代码找到可能触发的场景
搜索 hs_err_pid site:openjdk.java.net
可以看到几个相关的issue, 比如 JDK-8220786 和 JDK-8220787 都是讨论把 hs-err
从定向到 stdout 或 stderr 去.
接着继续搜索 github 上的 JDK 源码, 找到下面的代码:
https://github.com/openjdk/jdk/blob/4e5c25ee43d4ec31ed5160fd93a2fd15e35182f8/src/hotspot/share/utilities/vmError.cpp#L1827
fd_log = prepare_log_file(ErrorFile, "hs_err_pid%p.log", true, buffer, sizeof(buffer));
从这里我们看到了如何写入以及什么时候才能写入 hs_err_pid%p.log
的逻辑. 这个代码位于: vmError.cpp
的 VMError::report_and_die
方法, 并且这里面有好几个重载的该方法.
接下来, 我们只要在该 repo 里面搜索什么时候调用这个 report_and_die
方法的, 就能找到我们寻找的触发条件.
其中一处是关于在 linux 平台触发的在 https://github.com/openjdk/jdk/blob/4e5c25ee43d4ec31ed5160fd93a2fd15e35182f8/src/hotspot/os/posix/signals_posix.cpp#L649, 这个方法是: JVM_HANDLE_XXX_SIGNAL
, 在其注释中提到 This routine may recognize any of the following kinds of signals: SIGBUS, SIGSEGV, SIGILL, SIGFPE, SIGQUIT, SIGPIPE, SIGXFSZ, SIGUSR1
.
所以, 我们可以使用 linux signal 来测试.
使用 SIGSEGV 测试
在 linux 机器上写一个 main 函数里面让线程 sleep 的代码, 然后启动它. 同时开另外一个窗口, 对它发送 SIGSEGV 信号, 观察到:
$ java -XX:OnError="touch /tmp/eric.txt" ErrorExample
$ kill -11 <pid>
发现 /tmp/eric.txt 确实产生了. 同时观察到在当前目录产生了 hs_err_pid702969.log
等日志文件. 虽然Java 进程奔溃会默认产生 core dump, 但是却没有找到 core dump.
为什么没有产生 core dump
JVM 有个启动参数 -XX:+CreateCoredumpOnCrash
默认是打开的, 所以, 理论上当 crash 的时候, 会产生core dump.
$ java -XX:+PrintFlagsFinal -version | grep Core
bool CreateCoredumpOnCrash = true {product} {default}
检查了 ulimit -c
确认是 unlimited
, 同时搜索了全磁盘, 没有找到对应的 core dump.
如果查看上面提到的 hs_err_pid702969.log
, 就会发现里面这么写的:
# Core dump will be written. Default location: Core dumps may be processed with "/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E" (or dumping to /home/supra/work/java/error/core.702969)
这里内容提及了 core dump, 以及 apport
和可能放置 core dump 的路径, 但该路径的core dump 却不存在.
apport 是什么
apport 是一款系统程序, 它能拦截应用在 crash 时候的 core dump 数据, 并且生成详细的report. 对于绝大多数用户来说, 遇到程序crash生成 core dump 并不能带来什么价值, 如果把 core dump 转成一份可以被用户或者admin 能读取的固定格式报告, 将会非常有用, apport 就是这么一种程序, 官方网站: https://wiki.ubuntu.com/Apport
如果你使用桌面版本的 Ubuntu 发行版, 在程序crash 的时候, 你可能会看到一个弹窗, 问你要不要发送crash report 给 ubuntu 官方, 它就是 apport 的结果.
通常它产生的 crash report 在 /var/crash/
文件夹.
为什么 core dump 被 apport 接管了?
查看 core dump 的官方文档: https://man7.org/linux/man-pages/man5/core.5.html.
可以看到: /proc/sys/kernel/core_pattern
是一个内核参数,用于控制核心转储文件的生成方式。当一个进程崩溃时,内核会查看 core_pattern 的值来决定如何处理核心转储。
core_pattern 可以包含以下几种类型的值:
- 静态文件路径:如果 core_pattern 包含一个普通的文件路径(不包含管道符 |),核心转储文件将被写入该路径指定的位置。路径中可以包含一些特殊的格式化字符(如 %p 表示进程 ID),这些字符会被替换为相应的值,以便为每个核心转储生成唯一的文件名。
- 管道命令:如果 core_pattern 的值以管道符 | 开头,后面跟随的是一个命令,那么内核会执行该命令,并将核心转储数据作为标准输入传递给它。这允许将核心转储数据发送到一个自定义的处理程序,如 Apport 或 Systemd-coredump,而不是直接写入文件系统。
如: /var/coredumps/core.%e.%p
或 |/usr/share/apport/apport %p %s %c %d %P %u %g -- %E
我实验用的 linux 版本就是上面的带管道的例子, 所以, 它使用 apport 生成了 crash dump, 我去 /var/crash/
目录, 就看到了 crash report _usr_lib_jvm_java-11-openjdk-amd64_bin_java.1000.crash
.
现在如何产生 core dump
- 使用
-XX:OnError="gdb %p"
- 修改
/proc/sys/kernel/core_pattern
为 core dump 文件模版(去除管道).
$ java -XX:OnError="gcore %p" ErrorExample
$ ls -alh | grep core
$ core.720444
总结
-XX:OnError
决定当致命错误是干啥-XX:ErrorFile
决定了 hs_err
日志的文件名, 还有另外两个类似的参数决定是不是写到 stdout 和 stderr.-XX:+CreateCoredumpOnCrash
决定了崩溃时默认产生core dump, core dump 文件名或怎么处理根据 /proc/sys/kernel/core_pattern
来定.