JNI 中错误的信号处理导致 JVM 崩溃问题分析
编者按:JNI 是 Java 和 C 语言交互的主要手段,要想做好 JNI 的编程并不容易,需要了解 JVM 内部机理才能避免一些错误。本文分析 Cassandra 使用 JNI 本地库导致 JVM 崩溃的一个案例,最后定位问题根源是信号的错误处理(一些 C 编程人员经常会截获信号,做一些额外的处理),该案例提示 JNI 编程时不要随意截获信号处理。
现象
在使用 Cassandra 时遇到运行时多个位置都有发生 crash 现象,并且没有 hs_err 文件生成,这里列举了其中一个 crash 位置:
分析
首先直接基于上面这个 crash 的 core 文件展开分析,下面分别是对应源码上下文和指令上下文:
使用 GDB 调试对应的 core 文件,如下图所示:
在 GDB 中进行单步调试(GDB 调试可以参考官方文档),配合源代码发现 crash 的原因是传入的 name 为 null,导致调用 name.split("\_") 时触发了 SIGSEGV 信号,直接 crash。暂时抛开这个方法传入 name 为 null 是否有问题不论,从 JVM 运行的机制来说,这里有个疑问,遇到一个 Null Pointer 为什么不是抛出 Null Pointer Exception(简称 NPE)而是直接 crash 了呢?
这里有一个知识需要普及一下:Java 层面的 NPE 主要分为两类,一类是代码中主动抛出 NPE 异常,并被 JVM 捕获 (这里的代码既可以是 Java 代码,也可以是 JVM 内部代码);另一类隐式 NPE(其原理是 JVM 内部遇到空指针访问,会产生 SIGSEGV 信号, 在 JVM 内部还会检查运行时是否存在 SIGSEGV 信号)。
带着上面的疑问,又看了几处其他位置的 crash,发现都是因为对象为 null 导致的 SIGSEGV,却都没有抛出 NPE,而是直接 crash 了,再结合都没有 hs_err 文件生成的现象, hs_err 文件生成功能位于 JVM 的 SIGSEGV 信号处理函数中,代码如下:
由于 hs_err 文件没有产生,一个很自然的推断:Cassandra 运行中可能篡改了或者捕获了 SIGSEGV 信号,并且可能做了处理,以至于 JVM 无法正常处理 SIGSEGV 信号。
然后排查业务方是否在 Cassandra 中用到了自定义的第三方 native 库,果然笔者所猜测的,有两个 native 库里都对 SIGSEGV 信号做了捕获,注释掉这些代码后重新跑对方的业务,crash 现象不再发生,问题(由于 Cassandra 中对 NPE 有异常处理导致 JVM 崩溃)解决。
总结
C/C++ 的组件在配合 Java 一起使用时,需要注意的一点就是不要随意去捕获系统信号,特别是 SIGSEGV、SIGILL、SIGBUS 等,因为会覆盖掉 JVM 中的信号捕获逻辑。附录 这里贴一个 demo 可以用来复现 SIGSEGV 信号覆盖造成的后果,有兴趣的可以跑一下:
// JNITest.java
import java.util.UUID;
public class JNITest {
public static void main(String[] args) throws Exception {
System.loadLibrary("JNITest");
UUID.fromString(null);
}
}
// JNITest.c
#include <signal.h>
#include <jni.h>
JNIEXPORT
jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
signal(SIGSEGV, SIG_DFL);//如果注释这条语句,在运行时会出现 NullPointerExcetpion 异常
return JNI_VERSION_1_8;
}
通过 GCC 编译并执行就可以触发相同的问题,编译执行命令如下:
$ gcc -Wall -shared -fPIC JNITest.c -o libJNITest.so -I$JAVA_HOME/include -I$JAVA_HOME/include/linux
$ javac JNITest.java
\$ java -Xcomp -Djava.library.path=./ JNITest
文章转载自公众号:openEuler