文档章节

用 JNI 进行 Java 编程(4)

Jerikc
 Jerikc
发布于 2012/10/08 23:38
字数 2561
阅读 248
收藏 2

高级主题

概述

从 Java 程序内调用本机代码破坏了 Java 程序的可移植性和安全性。尽管已编译的 Java 字节码保持了很好的可移植性,但必须为您打算用来运行该应用程序的每个平台重新编译本机代码。另外,由于本机代码在 JVM 之外执行,所以约束它的安全性协议不必和 Java 代码的相同。

从本机程序调用 Java 代码也很复杂。因为 Java 语言是面向对象的,所以从本机应用程序调用 Java 代码通常涉及面向对象技术。有些本机语言不支持面向对象编程或只是有限地支持面向对象编程(譬如 C),使用这些语言调用 Java 方法可能会产生问题。在本节中,我们将讨论使用 JNI 所带来的若干复杂性,并研究解决它们的方法。

Java 字符串 vs. C 字符串

Java 字符串是作为 16 位 Unicode 字符存储的,而 C 字符串是作为一组 8 位且以空字符为结束的字符存储的。JNI 提供了几个有用的函数,它们用于在 Java 字符串和 C 字符串之间进行转换并操作这两种字符串。下面的代码片段演示了如何将 C 字符串转换成 Java 字符串:


/* Convert a C string to a Java String. */
char[]  str  = "To be or not to be.\n";
jstring jstr = (*env)->NewStringUTF(env, str);

接下来,我们研究将 Java 字符串转换成 C 字符串的代码。请注意第 5 行对 ReleaseStringUTFChars() 函数的调用。当您不再使用 Java 字符串时,应该使用这个函数来释放它们。当本机代码不再需要引用字符串时,请总是确保释放它们。不这样做可能导致内存泄漏。


/* Convert a Java String into a C string. */
char buf[128; 
const char *newString = (*env)->GetStringUTFChars(env, jstr, 0);
...
(*env)->ReleaseStringUTFChars(env, jstr, newString);

Java 数组 vs. C 数组

与字符串类似,Java 数组和 C 数组在内存中的表示不同。幸运的是,一组 JNI 函数可以提供指向数组中元素的指针。下图显示了如何将 Java 数组映射到 JNI C 类型。

C 类型 jarray 表示通用数组。在 C 语言中,所有数组类型实际上只是 jobject 的同义类型。但是,在 C++ 语言中,所有的数组类型都继承了 jarrayjarray 又依次继承了 jobject 。有关所有 C 类型对象的继承图,请参阅附录 A:JNI 类型。

使用数组

通常,处理数组时,首先想到要做的是确定其大小。为了做到这一点,应该使用 GetArrayLength() 函数,它返回一个表示数组大小的 jsize

接下来,会想要获取一个指向数组元素的指针。可以使用 GetXXXArrayElement()SetXXXArrayElement() 函数(根据数组的类型替换方法名中的 XXXObjectBooleanByteCharIntLong 等等)来访问数组中的元素。

当本机代码完成了对 Java 数组的使用时,必须调用函数 ReleaseXXXArrayElements() 来释放它。否则,可能导致内存泄漏。下面的代码段显示了如何循环遍历一个整型数组的所有元素:


/* Looping through the elements in an array. */
int* elem = (*env)->GetIntArrayElements(env, intArray, 0);
for (i=0; I < (*env)->GetIntArrayLength(env, intArray); i++)
   sum += elem[i]
(*env)->ReleaseIntArrayElements(env, intArray, elem, 0);

局部引用 vs. 全局引用

当使用 JNI 编程时,会需要使用对 Java 对象的引用。缺省情况下,JNI 创建局部引用以确保它们可以被垃圾收集。由于这一点,您可能会因为尝试存储一个本地引用,以便稍后重用它而无意间编写出非法代码,如下面的代码样本所示:


/* This code is invalid! */
static jmethodID mid;
 
JNIEXPORT jstring JNICALL
Java_Sample1_accessMethod(JNIEnv *env, jobject obj)
{
    ...
    cls = (*env)->GetObjectClass(env, obj);
    if (cls != 0)
    mid = (*env)->GetStaticMethodID(env, cls, "addInt", "(I)I");
    ...
}

因为第 10 行的错误,所以这个代码段是非法的。midmethodID,并且 GetStaticMethodID() 返回 methodID。但是,返回的 methodID 是一个局部引用,而您不应该将一个局部引用赋给全局引用。而 mid 是一个全局引用。

Java_Sample1_accessMethod() 返回之后,mid 引用就不再有效,因为赋给它现在超出作用域以外的局部引用。尝试使用 mid 将导致错误结果或 JVM 崩溃。

创建全局引用

要纠正这个问题,需要创建和使用全局引用。全局引用将在显式释放之前一直有效,您必须记住去显式地释放它。没有释放引用可能导致内存泄漏。

使用 NewGlobalRef() 创建全局引用,并用 DeleteGlobalRef() 删除它,如下面的代码样本所示

/* This code is valid! */
static jmethodID mid;
  
JNIEXPORT jstring JNICALL
 Java_Sample1_accessMethod(JNIEnv *env, jobject obj)
{
   ...
   cls = (*env)->GetObjectClass(env, obj);
   if (cls != 0)
   {
      mid1 = (*env)->GetStaticMethodID(env, cls, "addInt", "(I)I");
      mid = (*env)->NewGlobalRef(env, mid1);
      ...
}

错误处理

在 Java 程序中使用本机方法,就以某种基本的方式破坏了 Java 安全性模型。因为 Java 程序在一个受控的运行时系统(JVM)中运行,所以 Java 平台设计师决定通过检查常见运行时系统错误(如数组下标、越界错误、空指针错误)来帮助程序员。从另一方面讲,由于 C 和 C++ 不使用此类错误检查,所以本机方法程序员必须自己处理所有错误情况,而在运行时,这些错误可以在 JVM 中被捕获。

例如,对于 Java 程序而言,通过抛出一个异常来向 JVM 报告出错是常见和正确的操作。C 没有异常,因此必须使用 JNI 的异常处理函数。

JNI 的异常处理函数

有两种方法用来在本机代码中抛出异常:可以调用 Throw() 函数或 ThrowNew() 函数。在调用 Throw() 之前,首先需要创建一个 Throwable 类型的对象。可以通过调用 ThrowNew() 跳过这一步,因为这个函数为您创建了该对象。在下面的示例代码片段中,我们使用这两个函数抛出 IOException


/* Create the Throwable object. */
jclass cls = (*env)->FindClass(env, "java/io/IOException");
jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
jthrowable e = (*env)->NewObject(env, cls, mid);
 
/* Now throw the exception */
(*env)->Throw(env, e);
 ...
 
/* Here we do it all in one step and provide a message*/
 (*env)->ThrowNew(env,(*env)->FindClass("java/io/IOException"),
                  "An IOException occurred!");

Throw()ThrowNew() 函数并不中断本机方法中的控制流。直到本机方法返回,在 JVM 中才会将异常实际抛出。在 C 中,一旦碰到错误条件,不能使用 Throw()ThrowNew() 函数立即退出方法,而在 Java 中,这可以使用 throw 语句来退出方法。相反,需要在 Throw()ThrowNew() 函数之后立即使用 return 语句,以便在出错点退出本机方法。

JNI 的异常捕获函数

当从 C 或 C++ 调用 Java 时,也可能需要捕获异常。 许多 JNI 函数都能抛出希望捕获的异常。ExceptionCheck() 函数返回 jboolean 以表明是否抛出了异常,而 ExceptionOccured() 方法返回指向当前异常的 jthrowable 引用(或者返回 NULL,如果未抛出异常的话)。

如果正在捕获异常,可能要处理异常,在这种情况下需要在 JVM 中清除该异常。可以使用 ExceptionClear() 函数来进行这个操作。ExceptionDescribed() 函数用来显示异常的调试消息。

本机方法中的多线程

在使用 JNI 工作时,您将遇到的更高级的问题之一是在本机方法中使用多线程。即使是在不需要支持多线程的系统上运行时,Java 平台也是作为多线程系统来实现的;因此您有责任确保本机函数是线程安全的。

在 Java 程序中,可以通过使用 synchronized 语句实现线程安全的代码。synchronized 语句的语法使您能够获取对象上的锁。 只要在 synchronized 块中,就可以执行任何数据操作,而不必担心其它线程会悄悄进入并访问您锁定的对象。

JNI 使用 MonitorEnter()MonitorExit() 函数提供类似的结构。对于传递到 MonitorEnter() 函数中的对象,您会得到一个用于该对象的监视器(锁),并在使用 MonitorExit() 函数释放它之前一直持有该锁。对于您锁定的对象而言,MonitorEnter()MonitorExit() 函数之间的所有代码保证是线程安全的。

本机方法中的同步

下表显示了如何在 Java、C 和 C++ 中同步一块代码。正如您所见,这些 C 和 C++ 函数类似于 Java 代码中的 synchronized 语句。


XML error: The image is not displayed because the width is greater than the maximum of 580 pixels. Please decrease the image width.


随本机方法一起使用 synchronized

确保本机方法同步的另一种方法是:当在 Java 类中声明 native 方法时使用 synchronized 关键字。

使用 synchronized 关键字将确保任何时候从 Java 程序调用 native 方法,它都将是 synchronized。 尽管用 synchronized 关键字来标记线程安全的本机方法是个好想法,但通常最好总是在本机方法实现中实现同步。这样做的主要原因如下:

  • C 或 C++ 代码和 Java 本机方法声明不同,因此,如果方法声明有变动(即,如果一旦除去了 synchronized 关键字),此方法可能马上不再是线程安全的了。

  • 如果有人对使用该函数的其它本机方法(或其它 C 或 C++ 函数)进行编码,他们可能并没有意识到该本机实现不是线程安全的。

  • 如果将函数作为普通的 C 函数在 Java 程序之外使用,则它不是线程安全的。

其它同步技术

Object.wait()Object.notify()Object.notifyAll() 方法也支持线程同步。因为所有 Java 对象都将 Object 类作为父类,所以所有 Java 对象都有这些方法。您可以象调用其它方法一样,从本机代码调用这些方法,并以 Java 代码中相同的方式来使用它们,以实现线程同步。

本文转载自:http://www.ibm.com/developerworks/cn/education/java/j-jni/section4.html

共有 人打赏支持
Jerikc
粉丝 91
博文 246
码字总数 22757
作品 0
浦东
程序员
私信 提问
用 JNI 进行 Java 编程(1)

本教程是关于什么的? Java 本机接口(Java Native Interface (JNI))是一个本机编程接口,它是 Java 软件开发工具箱(Java Software Development Kit (SDK))的一部分。JNI 允许 Java 代码使...

Jerikc
2012/10/08
0
0
用 JNI 进行 Java 编程(5)

结束语和参考资料 结束语 Java 本机接口是 Java 平台中一种设计良好和良好集成的 API。它被设计成用来使您能将本机代码合并到 Java 程序中,也为您提供了一种在用其它编程语言编写的程序中使...

Jerikc
2012/10/08
0
0
学习——>JNI设置C++与java的结合

JNI是Java Native Interface的英文缩写, 中文翻译为本地调用, 自从Java 1.1开始就成为了Java标准的一部分. C/C++是系统级的编程语言, 可以用来开发任何和系统相关的程序和类库, 但是Java本身...

home-kevin
2014/01/07
0
0
Android JNI学习(三)——Java与Native相互调用

本系列文章如下: Android JNI(一)——NDK与JNI基础 Android JNI学习(二)——实战JNI之“hello world” Android JNI学习(三)——Java与Native相互调用 Android JNI学习(四)——JNI的常用方法...

隔壁老李头
05/09
0
0
native关键字初识--java调用非java代码的接口

Java基础知识——JNI入门介绍(上) Java™ 本机接口(Java Native Interface,JNI)是一个标准的 Java API,它支持将 Java 代码与使用其他编程语言编写的代码相集成。如果您希望利用已有的代...

成长中的菜鸟
2015/02/10
0
0

没有更多内容

加载失败,请刷新页面

加载更多

一个案例彻底弄懂如何正确使用 mysql inndb 联合索引

摘要: 有一个业务是查询最新审核的5条数据 ```sql SELECT `id`, `title` FROM `th_content` WHERE `audit_time` < 1541984478 AND `status` = 'ONLINE' ORDER BY `audit_time` D. 原来链接 ......

阿里云官方博客
16分钟前
1
0
详解如何用爬虫采集视频播放量数据(以腾讯视频为例)

现代社会提到大数据大家都知道这是近几年才形成的对于数据相关的新名词,在1980年,著名未来学家阿尔文·托夫勒便在 《第三次浪潮》一书中,将大数据热情地赞颂为“第三次浪潮的 华彩乐章”...

技术阿飞
21分钟前
2
0
区块链时代的拜占庭容错:Tendermint(二)

原文题目:《Tendermint: Byzantine Fault Tolerance in the Age of Blockchains》 原文作者:Ethan Buchman 翻译:饶云坤 校对:傅晓波 本文为节选 以下为正文: 本章阐述Tendermint共识算法...

万向区块链
34分钟前
0
0
AS连接网易Mumu模拟器

1、安装模拟器 打开这个网址现在模拟器然后安装 http://mumu.163.com/ 2、安装完成后启动模拟器 3、进入模拟器安装目录 例如本机的安装目录:C:\Program Files (x86)\MuMu\emulator\nemu\vmo...

HGMrWang
41分钟前
9
0
设计要做到扩展性强还挺难的

概述 在日常开发中,有时候你的上司会跟你说,这个模块的设计扩展性要高。把这句话说出来很简单,但是要做到则非常难。导致难的其中一个因素是: 你不熟悉这个行业的业务的玩法 我举个例子来...

Sam哥哥聊技术
43分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部