JNI 避免因为本地C/C++代码崩溃而引发虚拟机终止
目录
前言
上文我们说过,由于Java调用的代码是其他语言实现的,这样会带来很多不可控的因素,例如在C/C++
代码中,我们常常会因为访问了空指针而导致segmentation fault
,最终导致程序提前结束。
而Java调用了一个发生了segmentation fault
的动态链接库时,JVM也会提前结束程序,当发生这种情况时,JVM
层面是无法通过捕获异常的方式避免的。
模拟错误
我们模拟一下这个过程,我们本地的C/C++
代码如下:
#include "top_yalexin_jni_HelloJNI.h"
void generateSegmentationFault() {
int* src = NULL;
*src = 0;
}
int times2(int value) {
if (value > 10) {
generateSegmentationFault();
}
return value * value;
}
int getPowWrapper(int value) {
return times2(value);
}
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv* env, jobject thisObj, jint value) {
return getPowWrapper(value);
}
我们的Java代码如下:
package top.yalexin.jni;
public class HelloJNI {
static {
// helloJNI.dll in Windows, libhelloJNI.so in Unixes
System.loadLibrary("helloJNI");
}
private native int pow2(int value);
public static void main(String[] args) {
System.out.println("====== start ======");
HelloJNI helloJNI = new HelloJNI();
try {
System.out.println("pow2(5) -> " + helloJNI.pow2(5));
} catch (Exception e) {
System.out.println(e);
}
try {
System.out.println("pow2(15) -> " + helloJNI.pow2(15));
} catch (Exception e) {
System.out.println(e);
}
System.out.println("====== end ======");
}
}
按照上一篇文章的流程,我们在Windows上面的执行结果为:
java -Djava.library.path=top\yalexin\jni top.yalexin.jni.HelloJNI
====== start ======
pow2(5) -> 25
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007fff20f116b8, pid=25444, tid=28040
#
# JRE version: Java(TM) SE Runtime Environment (17.0.5+9) (build 17.0.5+9-LTS-191)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (17.0.5+9-LTS-191, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, windows-amd64)
# Problematic frame:
# C [helloJNI.dll+0x116b8]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# C:\Users\Yalexin\IdeaProjects\hello-world\src\hs_err_pid25444.log
#
# If you would like to submit a bug report, please visit:
# https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
在Linux平台上面显示为:
java -Djava.library.path='/root/jni' top.yalexin.jni.HelloJNI
====== start ======
pow2(5) -> 25
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007fa7793d87b5, pid=6049, tid=0x00007fa77cce4700
#
# JRE version: Java(TM) SE Runtime Environment (8.0_411) (build 1.8.0_411-b09)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.411-b09 mixed mode linux-amd64 compressed oops)
# Problematic frame:
# C [libhelloJNI.so+0x7b5] generateSegmentationFault()+0x10
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /root/jni/hs_err_pid6049.log
#
# If you would like to submit a bug report, please visit:
# http://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
[1] 6049 abort java -Djava.library.path='/root/jni' top.yalexin.jni.HelloJNI
我们可以看到,我们的Java虚拟机也提前被迫中断了,有没有什么办法能够避免这种情况呢?
Windows系统解决方案
还是借助Visual Studio,具体使用方案参照上一篇文章,我们只要把最外面的代码使用捕获异常的方式即可:
#include "top_yalexin_jni_HelloJNI.h"
#include <Windows.h>
void generateSegmentationFault() {
int* src = NULL;
*src = 0;
}
int times2(int value) {
if (value > 10) {
generateSegmentationFault();
}
return value * value;
}
int getPowWrapper(int value) {
return times2(value);
}
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv* env, jobject thisObj, jint value) {
__try {
return getPowWrapper(value);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// 当 segmentation fault 出现时, 抛出 Java exception
jclass exc = env->FindClass("java/lang/RuntimeException");
if (exc != NULL)
{
env->ThrowNew(exc, "Segmentation fault occurred in native code pow2");
}
return -1;
}
}
再次执行代码,我们发现,我们的代码成功捕获了该异常:
java -Djava.library.path=top\yalexin\jni top.yalexin.jni.HelloJNI
====== start ======
pow2(5) -> 25
java.lang.RuntimeException: Segmentation fault occurred in native code pow2
====== end ======
Linux系统解决方案
在Linux上面,上述代码无法执行,这是因为Windows.h
是Windows下专有的头文件。
但是可以可借助信号量机制来完成:
#include "top_yalexin_jni_HelloJNI.h"
#include <signal.h>
#include <setjmp.h>
// 定义全局变量用于存储跳转环境
static jmp_buf jump_buffer;
// 信号处理函数
void handle_sigsegv(int sig) {
if (sig == SIGSEGV) {
// 执行非本地跳转,恢复到设置的环境
longjmp(jump_buffer, 1);
}
}
void generateSegmentationFault() {
int* src = NULL;
*src = 0;
}
int times2(int value) {
if (value > 10) {
generateSegmentationFault();
}
return value * value;
}
int getPowWrapper(int value) {
return times2(value);
}
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv* env, jobject thisObj, jint value) {
// 设置跳转环境, setjmp 返回0表示直接调用,非零表示从 longjmp 跳转回来
if (setjmp(jump_buffer) == 0) {
// 设置信号处理函数
signal(SIGSEGV, handle_sigsegv);
// 下面的代码有可能引发访问违例
return getPowWrapper(value);
// 如果没有违例(segmentation fault),则按照既定逻辑返回
}
else {
// 当 segmentation fault 出现时, 抛出 Java exception
jclass exc = env->FindClass("java/lang/RuntimeException");
if (exc != NULL)
{
env->ThrowNew(exc, "Segmentation fault occurred in native code pow2");
}
return -1;
}
}
再次运行,我们发现,我们的代码成功捕获了该异常:
java -Djava.library.path='/root/jni' top.yalexin.jni.HelloJNI
====== start ======
pow2(5) -> 25
java.lang.RuntimeException: Segmentation fault occurred in native code pow2
====== end ======
总结
实际应用中,我们调用的.dll
或者.so
文件可能内部逻辑非常复杂,这就需要我们对别人的动态链接库做进一步封装,然后我们自己在C/C++
代码中主动捕获异常信息,然后把异常信息给到Java代码中做进一步处理。
本文由「黄阿信」创作,创作不易,请多支持。
如果您觉得本文写得不错,那就点一下「赞赏」请我喝杯咖啡~
商业转载请联系作者获得授权,非商业转载请附上原文出处及本链接。
关注公众号,获取最新动态!
历史评论
开始评论