调用JNI函数时,JVM在垃圾回收期间崩溃

4
我们有一个Java应用程序,它具有一个JNI层,该层是多线程的(pthread),并且在从底层网络接收到消息时将回调到Java级别。
我们注意到每次崩溃都是由gc引起的。我们甚至可以通过在JNI层从网络接收消息时手动触发gc来模拟这样的崩溃。
鉴于我们在此帖子https://dev59.com/L5rga4cB1Zd3GeqPo5M4#39401467中阅读到的有关JVM在GC期间行为的信息,我们仍然无法弄清楚为什么此类崩溃与gc有关,因为在gc期间会阻止JNI函数调用。
如果有人能够解释一下,那就太好了。谢谢提前。
以下是我们在应用程序崩溃后收集的堆栈跟踪。
Program terminated with signal 6, Aborted.
#0  0x0000003cdce325e5 in raise () from /lib64/libc.so.6
#1  0x0000003cdce33dc5 in abort () from /lib64/libc.so.6
#2  0x00007fdafe2516b5 in os::abort(bool) () from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#3  0x00007fdafe3efbf3 in VMError::report_and_die() ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#4  0x00007fdafde2f3e2 in report_vm_error(char const*, int, char const*, char const*) ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#5  0x00007fdafe24c1ff in os::PlatformEvent::park() ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#6  0x00007fdafe20c538 in Monitor::ILock(Thread*) ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#7  0x00007fdafe20c73f in Monitor::lock_without_safepoint_check() ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#8  0x00007fdafe2e7a1f in SafepointSynchronize::block(JavaThread*) ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#9  0x00007fdafe39bcdd in JavaThread::check_safepoint_and_suspend_for_native_trans(JavaThread*) ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#10 0x00007fdafe0123d8 in jni_NewByteArray ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#11 0x00007fdaa447b7d1 in JNIEnv_::NewByteArray (this=0x7fdaf800c9f8, len=7)
    at /usr/java/jdk1.8.0_65/include/jni.h:1643
---omitted---
#19 0x0000003cdd20b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
#20 0x00007fdafe24c133 in os::PlatformEvent::park() ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#21 0x00007fdafe20ce27 in Monitor::IWait(Thread*, long) ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#22 0x00007fdafe20d5f0 in Monitor::wait(bool, long, bool) ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
---Type <return> to continue, or q <return> to quit---
#23 0x00007fdafe39ed51 in Threads::destroy_vm() ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#24 0x00007fdafdfff931 in jni_DestroyJavaVM ()
   from /usr/java/jdk1.8.0_65/jre/lib/amd64/server/libjvm.so
#25 0x00007fdafe91a63d in JavaMain () from /usr/java/jdk1.8.0_65/bin/../lib/amd64/jli/libjli.so
#26 0x0000003cdd207aa1 in start_thread () from /lib64/libpthread.so.0
#27 0x0000003cdcee8aad in clone () from /lib64/libc.so.6

我们获取JNIEnv*的方式
JNIEnv *env = 0;
jint result = jvm->GetEnv((void **) &env, JNI_VERSION_1_8);
if (result != JNI_OK) {
    result = jvm->AttachCurrentThread((void **) &env, NULL);

没有您的本地代码,很难甚至不可能给出任何有意义的答案。 - Andrew Henle
1个回答

9
经过数天的调查,我们终于找到了这个JNI问题的原因。我们想在此分享我们的经验,希望能对其他人有所帮助。
首先,我们需要使用JNI的原因是因为我们需要使用一个第三方网络库,它是一个Linux本地库,而不幸的是,这正是我们问题的根源。
该库向我们提供了一个回调句柄,我们实现了它以接收来自该库的传入网络消息,并且我们后来发现,这个回调其实就是一个信号处理程序。这意味着无论何时出现信号,甚至在gc期间,都会调用此信号处理程序。
由于C线程在JVM的安全点期间继续运行,如果这些C线程未附加到JVM,那么情况就会很好,否则灾难肯定会降临。
以下是我们认为发生的情况(以下所有情况都发生在JNI层):
1.应用程序启动。我们初始化并缓存JNI资源,例如Jmv*,Method ID等。
2.我们向库注册一个C函数来接收消息。这个C函数是一个调用JNI API分配内存以容纳接收到的信息并将其传递到Java的函数。之后,我们开始等待传入的信息。
3.当消息最终到达时,将调用上述C函数来处理消息,但是等等...处理回调的线程是什么。这将是主线程或任何可用线程。
4.在任何JNI教科书中都会教授的方式,我们在调用任何JNI API之前首先将线程附加到JVM中。太棒了!
5.现在,在GC期间,所有Java线程都被阻止了,但是C层仍在运行。在这个关键时刻,如果有一条消息到达,某些线程(任何线程)将被调用以处理该消息。但是在gc期间还有哪些线程可用?所有应用程序线程都被阻止了,而唯一在此时仍在运行的线程(我们的猜测)不幸的是gc线程。
我们所看到的gdb堆栈跟踪基本上就是当一个gc线程实际上在堆上执行某些工作并且然后得到从应用程序调用来执行某些应用程序工作和几个JNI API调用时发生的事情...崩溃。
解决方案:
1.拥有一个处理库回调的C线程
2.永远不要将该线程附加到JVM
3.让其他线程连接到JVM以进行Native-Java转换。 p.s。也许一些细节并不完全准确,所以欢迎任何JVM专家的建议。我将尽力进行更正。
谢谢
更新.1(@apangin):
我们在这里有另一个gdb堆栈跟踪。只是想知道#18处的GangWorker是否为并行GC线程。
#0  0x00000035b90325e5 in raise () from /lib64/libc.so.6
#1  0x00000035b9033dc5 in abort () from /lib64/libc.so.6
#2  0x00007febd60813b5 in os::abort(bool) () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#3  0x00007febd6223673 in VMError::report_and_die() () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#4  0x00007febd60868bf in JVM_handle_linux_signal () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#5  0x00007febd607ce13 in signalHandler(int, siginfo*, void*) () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#6  <signal handler called>
#7  0x00007feb9fcf551c in JNIEnv_::NewByteArray (this=0x7febd001d9f8, len=8) at /usr/java/jdk1.8.0_131/include/jni.h:1643
*<omitted app specific calls>*
#13 <signal handler called>
#14 0x00000035b980b68c in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
#15 0x00007febd607b7e3 in os::PlatformEvent::park() () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#16 0x00007febd603c037 in Monitor::IWait(Thread*, long) () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#17 0x00007febd603c956 in Monitor::wait(bool, long, bool) () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#18 0x00007febd6244d6b in GangWorker::loop() () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#19 0x00007febd6082568 in java_start(Thread*) () from /usr/java/jdk1.8.0_131/jre/lib/amd64/server/libjvm.so
#20 0x00000035b9807aa1 in start_thread () from /lib64/libpthread.so.0
#21 0x00000035b90e8aad in clone () from /lib64/libc.so.6

1
如果你的发现是真实的,那么这就是一个JVM的bug:AttachCurrentThread不应该对GC线程成功。顺便说一下,从gdb的堆栈跟踪中应该清楚地看出一个线程是否运行GC代码。无论如何,你是正确的,JNI函数不能从信号处理程序中调用 - 它们不是信号安全的。 - apangin
我包含了另一个堆栈跟踪。我想知道 #18 处的 GangWorker 是什么。也许它是一个并行 GC 工作线程? - Kin Cheung
是的,这是一个GC线程。而且,AttachCurrentThread将当前线程强制转换为JavaThread而没有类型检查,但是GangWorker不是JavaThread的子类。 - apangin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接