(译文) JNI编程指南与规范5~8章节

1,252 阅读54分钟

第五章 本地和全局引用

JNI 将实例和数组类型(例如 jobject、jclass、jstring 和 jarray)公开为不透明引用。本地代码不能直接检查不透明引用指针的内容。而是通过 JNI 函数来获取不透明引用所指向的数据结构。通过处理不透明引用,你不必担心依赖于特定 Java 虚拟机的内部对象数据结构布局。但是,在 JNI 中,你需要了解更多有关于不同类型的引用:

  • JNI 支持三中透明引用:本地引用,全局引用和弱全局引用
  • 本地和全局引用拥有不同的生命周期。本地引用会被自动回收,而全局引用和弱全局引用会一直存在直到程序员将其释放
  • 一个本地或全局引用保持被引用的对象不会被垃圾回收。但是一个弱全局引用允许被引用的对象呗垃圾回收。
  • 并非所有的引用都可以在所有的上下文中使用。例如,在创建引用返回后的本地代码中使用本地引用是非法的。    在本章中,我们将详细讨论这些问题。 正确管理 JNI 引用对于编写可靠和节省空间的代码至关重要。

5.1 本地和全局引用

什么是本地和全局引用,以及他们有什么不同呢?我们会用一系列的实例来说明本地和全局引用。

5.1.1 本地引用

大多数 JNI 方法会创建本地引用。例如,JNI 方法 NewObject 创建一个新的实例对象并返回引用该对象的本地引用。

本地引用仅在创建它的本地方法的动态上下文中有效,并且仅在该方法的一次调用中有效。在本地方法执行期间创建的所有本地引用将在本地方法返回后被释放。

不能在本地方法中通过静态变量来储存本地引用,并在后续调用中使用相同的引用。例如下面的代码,是 4.4.4 节的 MyNewString 方法的修改版本,在这里使用了不正确的本地引用。

/* This code is illegal */ jstring
MyNewString(JNIEnv *env, jchar *chars, jint len) {
    static jclass stringClass = NULL;
    jmethodID cid;
    jcharArray elemArr;
    jstring result;
    if (stringClass == NULL) {
        stringClass = (*env)->FindClass(env, “java/lang/String”);
        if (stringClass == NULL) {
            return NULL;
        }
    }
    /* It is wrong to use the cached stringClass here, because it may be invalid. */
    cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");
    ...
    elemArr = (*env)->NewCharArray(env, len);
    ...
    result = (*env)->NewObject(env, stringClass, cid, elemArr);
    (*env)->DeleteLocalRef(env, elemArr);
    return result;
}

这里已经消除了和我们将要讨论的没有直接关系的行。在静态变量中缓存 stringClass 的目的可能是想消除重复执行如下函数调用的开销:

FindClass(env, "java/lang/String");

这不是正确的方法,因为 FindClass 返回一个指向 java.lang.String 类对象的本地引用。下面分析为什么这样会引发问题,假设 C.f 本地方法实现调用 MyNewString:

JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) {
    char *c_str = ...;
    ...
    return MyNewString(c_str);
}

在本地方法 C.f 调用返回后,虚拟机会释放所有在 Java_C_f 运行期间创建的本地引用。这些释放的本地引用包括对储存在 stringClass 变量中的类对象的本地引用。在后面,MyNewString 调用将会尝试使用一个无效的本地引用,这可能导致内存损坏或者引发系统崩溃。例如,如下的代码片段使两个连续的调用到 C.f 并导致 MyNewString 遇到无效的本地引用:

...
... = C.f(); // The first call is perhaps OK.
... = C.f(); // This would use an invalid local reference.
...

有两种方法能够使一个本地方法无效。如前所述,在本地方法返回后,虚拟机会自动的释放所有在该本地方法执行期间创建的本地引用。另外,程序员可能想使用 JNI 函数例如,DeleteLocalRef 来显示的管理本地引用的生命周期。

如果虚拟机会在本地方法返回后自动释放,为什么还需要显示释放本地引用呢?本地引用防止被引用的对象被垃圾收集器回收,一直持续到本地引用无效为止。例如,在 MyNewString 中的 DeleteLocalRef 调用允许数组对象 elemArr 立即被垃圾收集器回收。否则,虚拟机只会在 MyNewString 调用的本地方法返回后(例如上面的 C.f)释放 elemArr 对象。

一个本地引用在其销毁之前,可能传递给多个本地方法。例如,MyNewString 方法返回一个通过 NewObject 创建的字符串引用。然后由 MyNewString 的调用者决定是否释放由 MyNewString 返回的本地引用。在 Java_C_f 例子中,C.f 又作为本地方法调用的结果返回 MyNewString 的结果。在虚拟机从 JAVA_C_f 函数接收到本地引用后,他将底层字符串对象给 c.f 的调用者然后销毁最初由 JNI 函数 NewObject 创建的本地引用,然后销毁最初由 JNI 函数 NewObject 创建的本地引用。

地引用当然仅在创建它的线程中有效。在一个线程中创建的本地引用不能够在其他线程中使用。在本地方法中将本地引用储存在全局变量中,并期望在另一个线程中使用是一个编程错误。

5.1.2 全局引用

你可以在跨多个本地方法调用中使用全局变量。全局引用可以跨多线程使用并保持有效,直到程序员释放它为止。和本地引用一样,一个全局引用能够确保被引用的对象不会被垃圾收集器回收。

和本地引用不同的是,本地引用可以通过大多数 JNI 函数创建,但是全局引用只可以通过一个 JNI 方法(NewGlobalRef)创建。接下来的 MyNewString 版本显示如何使用全局引用。我们突出显示下面的代码和在上一节中错误地缓存本地引用的代码之间的区别:

/* This code is OK */
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
    static jclass stringClass = NULL;
    ...
    if (stringClass == NULL) {
        jclass localRefCls = (*env)->FindClass(env, "java/lang/String");
        if (localRefCls == NULL) {
            return NULL; /* exception thrown */
        }

<b>
        /* Create a global reference */
        stringClass = (*env)->NewGlobalRef(env, localRefCls);
        /* The local reference is no longer useful */
        (*env)->DeleteLocalRef(env, localRefCls);
        /* Is the global reference created successfully? */
        if (stringClass == NULL) {
            return NULL; /* out of memory exception thrown */
        }
</b>
    }
    ...
}

这个修改版本中将从 FindClass 返回的本地引用传送给 NewGlobalRef,该方法创建一个指向 java.lang.String 对象的全局引用。我们检查在删除 localRefCls 后 NewGlobalRef 是否成功创建了 stringClass,因为这两种情况下都需要删除本地引用 localRefCls。

5.1.3 弱全局引用

弱全局引用是在 Java 2 JDK 1.2 中新加入的。弱全局引用通过 NewGlobalWeakRef 创建,通过 DeleteGlobalWeakRef 释放。和全局引用一样,弱全局引用跨本地方法调用和跨线程调用依旧有效。而和全局引用不同的是,弱全局引用不能防止底层数据对象被垃圾收集器回收。

MyNewString 示例显示了如何缓存 java.lang.String 的全局引用。MyNewString 示例可以使用弱全局引用来缓存 java.lang.String 类。我们是使用全局引用还是弱全局引用并不重要,因为 java.lang.String 是一个系统类,永远不会被垃圾收集器回收。当本机代码缓存的引用不能使底层对象不被垃圾回收时,弱全局引用变得更加有用。例如,假设一个本地方法 mypks.MyCls.f 对类 mypks.MyCls2 进行缓存。在弱全局引用中缓存类仍然允许 mypkg.MyCls2 被卸载。

JNIEXPORT void JNICALL Java_mypkg_MyCls_f(JNIEnv *env, jobject self) {
    static jclass myCls2 = NULL;
    if (myCls2 == NULL) {
        jclass myCls2Local = (*env)->FindClass(env, "mypkg/MyCls2");
        if (myCls2Local == NULL) {
            return; /* can’t find class */
        }
        myCls2 = NewWeakGlobalRef(env, myCls2Local);
        if (myCls2 == NULL) {
            return; /* out of memory */
        }
    }
    ... /* use myCls2 */
}

我们假设 MyCls 和 MyCls2 有相同的生命周期(例如,它们可能是通过同一个类加载器加载的)。但是我们没有考虑到这样一个场景,在 MyCls 及其本地方法实现 Java_mypks_MyCls 仍在使用的情况下,MyCls2 卸载并在稍后重新加载。如果这种情况发生了,我们必须检查缓存的弱引用是否仍然指向一个存活的类对象,或者指向已经没垃圾收集器回收的类对象。下一节将介绍如何对弱全局引用执行此类检查。

5.1.4 引用比较

在给定的两个本地、全局、弱全局引用中,可以使用 IsSameObject 方法来检查它们是否引用同一个对象。例如:

(*env)->IsSameObject(env, obj1, obj2)

如果 obj1 和 obj2 引用同一个对象,那么这个方法就放回 JNI_TRUE(或者 1),否则返回 JNI_FALSE(或者 0)。

在 JNI 中,NULL 引用是指向 Java 虚拟机中的空对象,如果 obj 是被本地或者全局引用,则可以使用:

(*env)->IsSameObject(env, obj, NULL)

或者

obj == NULL

来确认 obj 是否引用一个空对象。

弱引用对象使用的规则有一些不同。NULL 弱引用引用空对象。 然而,IsSameObject 对于弱全局引用具有特殊用途。您可以使用 IsSameObject 来确定非 NULL 弱全局引用是否仍然指向一个活动对象。假设 wobj 是一个非 NULL 的弱全局引用。以下调用:

(*env)->IsSameObject(env, wobj, NULL)

如果 wobj 引用一个已经被回收的对象,则返回 JNI_TRUE,如果 wobj 仍然引用一个存活的对象,则返回 JNI_FALSE。

5.2 释放引用

除了被引用对象占用的内存外,每个 JNI 引用本身都会消耗一定量的内存。作为一个 JNI 程序员,你应该了解到你的程序在一个给定的时间内,将会使用的引用数量。特别是,你应该意识到你的程序在执行期间的某个时间点上,允许创建的本地引用数量的上限,即使这些本地引用最后会被虚拟机自动释放。暂时性的过多创建引用,可能会导致内存耗尽。

5.2.1 释放本地引用

在大多数情况下,在实现一个本地方法的时候,你不用过多的考虑释放本地对象。当本地方法返回到调用者处时,Java 虚拟机会为你释放它们。但是 JNI 程序员有时候应该显示的释放本地引用以避免内存占用过多。考虑一下情况:

你需要在一个本地方法调用中创建大量的本地引用。这可能导致 JNI 内部本地引用表溢出,因此立即删除那些不再需要的本地引用将是一个好方法。例如,在以下程序段中,本地代码有遍历一个大的字符串数组的可能。每次迭代后,本地代码应该显示的释放对字符串元素的本地引用。如下所示:

for (i = 0; i < len; i++) {
    jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
    ... /* process jstr */
    (*env)->DeleteLocalRef(env, jstr);
}
  • 你想编写一个从未知上下文中调用的函数。在第 4.3 节中显示的 MyNewString 示例说明使用 DeleteLocalRef 在函数中快速删除本地引用,否则每次调用 MyNewString 函数后都会分配两个本地引用。
  • 你的本地方法不会返回。一个本地方法可能进入无限事件调度循环中,释放在循环内创建的本地引用将是非常重要的,这样它们就不会无限积累,导致内存泄漏。
  • 你的本地方法访问一个大对象,因而需要创建该对象的本地引用。然后,native 方法在返回给调用者前可以进行额外的计算。对大对象的本地引用将阻止对象在本地方法返回前本垃圾收集器回收,即使对象不再在本地方法的剩余部分使用。例如在以下程序片段中,由于事先已经显示的调用 DeleteLocalRef 了,因此在执行函数 longyComputation 时,垃圾收集器可能会释放 lref 引用的对象。
/* A native method implementation */ JNIEXPORT void JNICALL
Java_pkg_Cls_func(JNIEnv *env, jobject this) {
    lref = ...                       /* a large Java object */
    ...                            /* last use of lref */
    (*env)->DeleteLocalRef(env, lref);
    lengthyComputation();           /* may take some time */
    return;                        /* all local refs are freed */
}

5.2.2 在 Java 2 JDK 1.2 中管理本地引用

Java 2 JDK 1.2 中提供了一组而外的函数用于管理本地应用的生命周期。这些函数是 EnsureLocalCapacity、NewLocalRef、PushLocalFrame 和 PopLocalFrame。

JNI 规范规定虚拟机能够自动确保每个本地方法能够创建至少 16 个本地引用。经验表明,除了与虚拟机中的对象进行复杂的交互外,对于大多数本地方法,这个数量已经足够。但是如果,需要创建而外的本地引用,那么本地方法可能会发出一个 EnsureLocalCapacity 调用,以确保有足够的本地引用空间。例如,上述示例的轻微变化为循环执行期间创建的所有本地参考提供足够的容量,如果有足够的内存可用:

/* The number of local references to be created is equal to the length of the array. */
if ((*env)->EnsureLocalCapacity(env, len)) < 0) {
    ... /* out of memory */
}
for (i = 0; i < len; i++) {
    jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
    ... /* process jstr */
    /* DeleteLocalRef is no longer necessary */
}

当然和之前立即删除本地引用的版本相比,上面的版本需要消耗更多的内存。或者,Push / PopLocalFrame 函数允许程序员创建本地引用的嵌套范围。 例如,我们也可以重写同样的例子,如下所示:

#define N_REFS ... /* the maximum number of local references used in each iteration */
for (i = 0; i < len; i++) {
    if ((*env)->PushLocalFrame(env, N_REFS) < 0) {
        ... /* out of memory */
    }
    jstr = (*env)->GetObjectArrayElement(env, arr, i);
    ... /* process jstr */
    (*env)->PopLocalFrame(env, NULL);
}

PushLocalFrame 为特定数量的本地引用创建一个新的范围。PopLocalFrame 破坏超出的范围,释放该范围内的所有本地引用。使用 Push/PopLocalFrame 的好处是它们可以管理本地引用的生命周期,而无需担心在执行过程中可能会创建的每个本地引用。在上面的例子中,如果处理 jstr 的计算创建了额外的本地引用,则这些本地引用将会在 PopLocalFrame 返回后被释放。

当你编写期望返回一个本地引用的实例程序时,NewLocalRef 函数很有用。我们将在 5.3 节中演示 NewLocalRef 函数的用法。

本地代码可能会创建超出默认容量 16 或者 PushLocalFrame 或者 EnsureLocalCapacity 调用中保留的容量的本地引用。虚拟机将会尝试分配本地引用所需的内存。然而不能保证,这些内存是可用的。如果分配内存失败,虚拟机将会退出。你应该为本地引用保留足够的内存和尽快释放本地引用以避免这种意外的虚拟机退出。

Java 2 JDK 1.2 提供了一个命令行参数-verbose:jni。当使能这个参数,虚拟机会报告超过预留容量的本地引用创建情况。

5.2.3 释放全局变量

当你的本地代码不再需要访问一个全局引用时,你应该调用 DeleteGlobalRef 方法。如果你忘记调用这个函数,虚拟机将无法通过垃圾收集器回收相应的对象,即使这个对象再也不会在系统的其他地方中使用。

当你的本地代码不再需要访问一个弱全局引用时,你应该调用 DeleteWeakGlobalRef 方法。如果你忘记调用这个函数,Java 虚拟机仍然能够通过垃圾收集器收集底层对象,当时将无法回收该弱全局引用对象占用的内存。

5.3 引用管理规范

我们现在已经准备好基于我们前面的几节介绍的内容,来处理在本地代码中管理 JNI 引用的规则。目的是消除不必要的内存使用和对象保留。

通常来说有两种本地代码,直接实现在任意上下文中使用的本地方法和效用函数的函数。

当编写直接实现本地方法时,你需要注意避免在循环中过多的创建本地引用以及由不返回的本地方法创建的不需要的本地引用。在本地方法返回后,留下最多 16 个本地引用由虚拟机删除是可以接受的。本地方法调用不能导致全局引用或者弱全局引用累积,因为全局引用和弱全局引用在本地方法返回后不会释放。编写本机实用程序函数时,必须注意不要在整个函数中的任何执行路径上泄漏任何本地引用。因为效用函数可以从意料之外的上下文重复调用,任何不必要的引用创建都可能导致内存溢出。

  • 当一个返回基本类型的函数被调用时,它不会产生额外的本地、全局、弱全局引用累积副作用。
  • 当一个返回引用类型的函数被调用,它不能有本地、全局、弱全局引用的额外累积,除非这个引用被当做返回值。    为了缓存的目的,一个函数创建一些全局或弱全局引用是可以接受的,因为仅在第一次调用的时候会创建这些引用。

如果一个函数返回一个引用,你应该使用函数规范的返回引用部分。他不应该在某些时候返回本地引用,而在其他时候返回全局引用。调用者需要知道函数的返回类型,以便正确的管理自己的 JNI 引用。例如,以下代码重复的调用了一个函数 GetInfoString。我们需要知道 GetInfoString 返回的引用类型,以便能够在每次迭代后正确释放返回的 JNI 引用。

while (JNI_TRUE) {
    jstring infoString = GetInfoString(info);
    ... /* process infoString */
    ??? /* we need to call DeleteLocalRef, DeleteGlobalRef, or DeleteWeakGlobalRef depending on the type of reference returned by GetInfoString. */
}

在 Java 2 JDK 1.2 中,NewLocalRef 函数经常用于确保函数返回一个本地引用。为了说明,让我们对 MyNewString 函数进行另一个(有点设计的)更改。以下版本在全局引用中缓存经常请求的字符串(例如“CommonString”):

jstring MyNewString(JNIEnv *env, jchar *chars, jint len) {
    static jstring result;

    /* wstrncmp compares two Unicode strings */
    if (wstrncmp("CommonString", chars, len) == 0) {
        /* refers to the global ref caching "CommonString" */
        static jstring cachedString = NULL;
            if (cachedString == NULL) {
                /* create cachedString for the first time */
                jstring cachedStringLocal = ... ;
                /* cache the result in a global reference */
                cachedString =(*env)->NewGlobalRef(env, cachedStringLocal);
            }
        return (*env)->NewLocalRef(env, cachedString);
    }
    ... /* create the string as a local reference and store in result as a local reference */
    return result;
}

正常的代码路径返回作为本地引用的字符串。正如前面解释的那样,我们必须在一个全局引用中保存缓存的字符串,让其能够被多个本地方法和多个线程访问。加粗显示的行创建一个新的引用对象,其引用同一个缓存在全局引中的对象。作为其调用者契约的一部分,MyNewString 经常返回一个本地引用。

Push/PopLocalFrame 方法对于管理本地引用的声明周期是非常简便的。如果你在一个本地方法的入口调用 PushLocalFrame,需要在本地方法返回前调用 PopLocalFrame 以确保所有在本地方法执行期间创建的本地引用都会被回收。Push/PopLocalFrame 函数是非常有效率的。强烈建议你使用它们。

如果你在函数的入口调用了 PushLocalFrame,记得在程序的所有退出路径上调用 PopLocalFrame。例如,下面的程序有一个 PushLocalFrame 调用,但是却需要多个 PopLocalFrame 调用。

jobject f(JNIEnv *env, ...)
{
    jobject result;
    if ((*env)->PushLocalFrame(env, 10) < 0) {
        /* frame not pushed, no PopLocalFrame needed */
        return NULL;
    }
    ...
    result = ...;
    if (...) {
        /* remember to pop local frame before return */
        result = (*env)->PopLocalFrame(env, result);
        return result;
    }
    ...
    result = (*env)->PopLocalFrame(env, result); /* normal return */ return result;
}

错误的放置 PopLocalFrame 调用回引起不确定的行为,例如导致虚拟机崩溃。

上面的例子也表明为什么有时指定 PopLocalFrame 的第二个参数是有用的。result 本地引用最初在由 PushLocalFrame 构造的新框架中创建的。PopLocalFrame 将其作为第二个参数,result 转换为前一贞中的新的本地引用,然后弹出最顶层的框架。

第六章 异常

我们已经遇到大量在本地代码中需要检查执行 JNI 方法后可能产生的错误。这一章将介绍本地代码如何从这些错误状况中检测和修复。

我们将会重点关注作为 JNI 函数调用的结果发生的错误,而不是在本地代码中发生的任意错误。如果一个本地方法进行了操作系统调用,则只需要按照文档说明的方式来检查系统调用中可能发生的错误。另一方面,如果本地方法想 Java API 方法进行回调,则必须按照本章中描述的步骤来正确的检查和修复方法执行期间可能产生的异常。

6.1 概述

我们通过一些列的例子来介绍 JNI 异常处理函数。

6.1.1 在本地代码中缓存和抛出异常

下面的程序显示如何定义一个会抛出异常的本地方法。CatchThrow 类定义了一个 doit 方法,并且表明该方法会抛出一个 IllegalArgumentException:

class CatchThrow {
    private native void doit() throws IllegalArgumentException;
    private void callback() throws NullPointerException {
        throw new NullPointerException("CatchThrow.callback");
    }

    public static void main(String args[]) {
        CatchThrow c = new CatchThrow();
        try {
            c.doit();
        } catch (Exception e) {
            System.out.println("In Java:\n\t" + e);
        }
    }

    static {
        System.loadLibrary("CatchThrow");
    }
}

CatchThrow.main 方法调用本地方法 doit,doit 的实现如下:

JNIEXPORT void JNICALL Java_CatchThrow_doit(JNIEnv *env, jobject obj) {
    jthrowable exc;
    jclass cls = (*env)->GetObjectClass(env, obj);
    jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V");
    if (mid == NULL) {
        return;
    }
    (*env)->CallVoidMethod(env, obj, mid);
    exc = (*env)->ExceptionOccurred(env);
    if (exc) {
        /* We don't do much with the exception, except that
        we print a debug message for it, clear it, and
        throw a new exception. */
        jclass newExcCls;
        (*env)->ExceptionDescribe(env);
        (*env)->ExceptionClear(env);
        newExcCls = (*env)->FindClass(env,"java/lang/IllegalArgumentException");
        if (newExcCls == NULL) {
            /* Unable to find the exception class, give up. */
            return;
        }
    (*env)->ThrowNew(env, newExcCls, "thrown from C code");
    }
}

搭配本地库运行这个程序可以得到如下输出:

java.lang.NullPointerException:
    at CatchThrow.callback(CatchThrow.java)
    at CatchThrow.doit(Native Method)
    at CatchThrow.main(CatchThrow.java)
In Java:
    java.lang.IllegalArgumentException: thrown from C code

这个回调方法抛出一个 NullPointerException 异常。当 CallVoidMethod 将控制权返回给本地方法后,本地代码通过 JNI 方法 ExceptionOccurred 会检测到这个异常。在我们的例子当中,当异常被检测到,本地方法通过调用 ExceptionDescribe 会输出一个关于这个异常的描述性信息,使用 ExceptionClear 方法清除这个异常并且抛出一个 IllegalArgumentException 异常作为替代。

通过 JNI(例如通过调用 ThrowNew)引起的挂起异常不会立刻破坏本地方法的执行。这和 Java 编程语言中异常的行为是不同的。当使用 Java 编程语言抛出一个异常的时候,Java 虚拟机会自动将控制流程转移到最近的符合异常类型的 try/catch 代码块中。然后 Java 虚拟机会清除这个挂起的异常并执行异常处理。相比之下,在异常发生之后,JNI 程序员必须显示的进行流程控制。

6.1.2 一个有用的辅助函数

抛出一个异常,首先需要查找这个异常的类然后调用 ThrowNew 方法。为了简化这个任务,我们可以编写一个抛出一个命名异常的有用函数:

void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg) {
    jclass cls = (*env)->FindClass(env, name);
    /* if cls is NULL, an exception has already been thrown */
    if (cls != NULL) {
        (*env)->ThrowNew(env, cls, msg);
    }
    /* free the local ref */
    (*env)->DeleteLocalRef(env, cls);
}

在本书中,JNU 表示 JNI Utilities。JNU_ThrowByName 首先通过 FindClass 方法找到异常的类。如果 FindClass 调用失败(返回 NULL),虚拟机必须抛出一个异常(例如 NoClassDefFoundError)。在本次 JNU_ThrowByName 不会尝试抛出另外一个异常。如果 FindClass 成功返回,我们通过调用 ThrowNew 抛出一个命名的异常。当 JNU_ThrowByName 调用返回的时,它保证有一个挂起的异常,尽管这个挂起的异常不一定是由 name 参数指定的。在这个方法中,我们确保删除引用异常类的本地引用。如果 FindClass 失败并返回 NULL,将 NULL 传递给 DeleteLocalRef 将会是一个空操作,这将是一个恰当的操作。

6.2 恰当的异常处理

JNI 程序员必须遇见所有可能的异常情况并且编写相应的代码检查和处理这些情况。适当的异常处理有时候是乏味的,但是为了提高程序的鲁棒性却是必须的。

6.2.1 异常检查

有两种方式检查是否有错误产生了。

(1)大多数 JNI 方法使用一个明显的返回值(例如 NULL)来表明产生了一个错误。返回错误值也意味着在当前线程中产生了一个挂起的异常。(在返回值中编码错误情况是 C 中的常见用法)

下面的例子中表明 GetFieldID 返回 NULL 值来检查错误情况。例子包含两个部分:类 Window 定义了一些实例字段(handle,length 和 width)并且有一个本地方法用来缓存这些字段的字段 ID。尽管这些字段确实已经在 Window 类中了,我们仍然需要检查 GetFieldID 可能返回的错误值,因为虚拟机可能无法分配足够的内容用于保存字段 ID。

/* a class in the Java programming language */
public class Window {
    long handle;
    int length;
    int width;
    static native void initIDs();
    static {
        initIDs();
    }
}

/* C code that implements Window.initIDs */
jfieldID FID_Window_handle;
jfieldID FID_Window_length;
jfieldID FID_Window_width;
JNIEXPORT void JNICALL Java_Window_initIDs(JNIEnv *env, jclass classWindow) {
    FID_Window_handle =(*env)->GetFieldID(env, classWindow, "handle", "J");
    if (FID_Window_handle == NULL) { /* important check. */
        return; /* error occurred. */
    }
    FID_Window_length =(*env)->GetFieldID(env, classWindow, "length", "I");
    if (FID_Window_length == NULL) { /* important check. */
        return; /* error occurred. */
    }
    FID_Window_width = (*env)->GetFieldID(env, classWindow, "width", "I");
    /* no checks necessary; we are about to return anyway */
}

(2)当使用一个 JNI 方法,它的返回值无法标记一个错误的产生的时候,本地代码就必须依赖引起异常来进行错误检查。在当前线程中,用于检查是否有挂起的异常的 JNI 函数是 ExceptionOccurred。(ExceptionCheck 在 Java 2 JDK 1.2 版本中加入。)例如,JNI 方法 CallIntMethod 不能通过编码一个错误情况来作为返回值,典型的错误情况返回值例如-1 和 NULL 都不能很好的工作,因为当他们调用这个方法时,这些都是合理的返回值。考虑有一个 Fraction 类,它的 floor 方法返回分数值的整数部分并且有其他的本地代码调用这个函数。

public class Fraction {
    // details such as constructors omitted
    int over, under;
    public int floor() {
        return Math.floor((double)over/under);
    }
}

/* Native code that calls Fraction.floor. Assume method ID
MID_Fraction_floor has been initialized elsewhere. */
void f(JNIEnv *env, jobject fraction) {
    jint floor = (*env)->CallIntMethod(env, fraction, MID_Fraction_floor);
    /* important: check if an exception was raised */
    if ((*env)->ExceptionCheck(env)) {
        return;
    }
    ... /* use floor */
}

当 JNI 函数返回不同的错误代码是,本地代码仍然可能通过显示调用类检查异常,例如 ExceptionCheck。不管怎么样,通过检查不同的返回值仍然是高效的。如果一个 JNI 方法返回其错误值,那么在当前线程后续处理中调用 ExceptionCheck 方法将保证返回 JNI_TRUE。

6.2.2 异常处理

本地代码可以通过两种方式处理挂起的异常:

  • 本地代码实现能够选择立即返回,在方法调用者处进行异常处理
  • 本地代码可以通过调用 ExceptionClear 来清除异常然后执行它自己的异常处理函数    在调用任何后续 JNI 函数之前,检查、处理和清除挂起的异常时非常重要的。在带有挂起的异常,尚未明确清理的异常时调用大多数 JNI 方法都有可能导致意外的结果。当在当前线程中有一个挂起的异常时,你仅可以安全的调用一小部分 JNI 方法,11.8.2 节列出了这些 JNI 函数的完整列表。一般来说,当存在一个挂起的异常时,你可以调用专门的 JNI 函数来处理异常和释放通过 JNI 暴露出来的各种虚拟机资源。

当异常发生时,经常有必要去释放各种资源。在下面的例子当中,本地方法首先通过一个 GetStringChars 调用获取字符串的内容。如果在后续的处理中产生错误,它会调用 ReleaseStringChars:

JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
    const jchar *cstr = (*env)->GetStringChars(env, jstr);
    if (c_str == NULL) {
        return;
    }
    ...
    if (...) { /* exception occurred */
        (*env)->ReleaseStringChars(env, jstr, cstr);
        return;
    }
    ...
    /* normal return */
    (*env)->ReleaseStringChars(env, jstr, cstr);
}

第一次调用 ReleaseStringChars 是在有挂起的线程出现的时候。本地方法实现释放字符串资源并在之后立即返回,而不需要首先清除异常。

6.2.3 有用的辅助函数中的异常

程序员编写有用的辅助函数时应特别注意确保异常传播到本地调用方法中。我们特别强调一下两点:

优选方案,辅助函数应该提供特殊的返回值指示发生了异常。这简化了调用者检查待处理异常的任务。 此外,辅助函数应遵循在管理异常代码是注意管理本地应用的规则。 为了说明,让我们介绍一个基于实例方法的名字和描述符执行回调的辅助函数:

jvalue JNU_CallMethodByName(JNIEnv *env, jboolean *hasException, jobject obj, const char *name, const char *descriptor, ...)
{
    va_list args;
    jclass clazz;
    jmethodID mid;
    jvalue result;
    if((*env)->EnsureLocalCapacity(env, 2) == JNI_OK) {
        clazz = (*env)->GetObjectClass(env, obj);
        mid = (*env)->GetMethodID(env, clazz, name, descriptor);
        if(mid) {
            const char *p = descriptor;
            /* skip over argument types to find out the
               return type */
            while (*p != ')') p++;
            /* skip ')' */
            p++;
      va_start(args, descriptor);
      switch (*p) {
            case 'V':
                (*env)->CallVoidMethodV(env, obj, mid, args);
                break;

            case '[':
            case 'L':
                result.l = (*env)->CallObjectMethodV(env, obj, mid, args);
                break;

            case 'Z':
                result.z = (*env)->CallBooleanMethodV(env, obj, mid, args);
                break;

            case 'B':
                result.b = (*env)->CallByteMethodV(env, obj, mid, args);
                break;

            case 'C':
                result.c = (*env)->CallCharMethodV(env, obj, mid, args);
                break;

            case 'S':
                result.s = (*env)->CallShortMethodV(env, obj, mid, args);
                break;

            case 'I':
                result.i = (*env)->CallIntMethodV(env, obj, mid, args);
                break;

            case 'J':
                result.j = (*env)->CallLongMethodV(env, obj, mid, args);
                break;

            case 'F':
                result.f = (*env)->CallFloatMethodV(env, obj, mid, args);
                break;

            case 'D':
                result.d = (*env)->CallDoubleMethodV(env, obj, mid, args);
                break;

            default:
                (*env)->FatalError(env, "illegal descriptor");
            }
            va_end(args);
        }
        (*env)->DeleteLocalRef(env, clazz);
    }
    if (hasException) {
        *hasException = (*env)->ExceptionCheck(env);
    }
    return result
}

除了其他参数以外,JNU_CallMethodByName 还有一个指向 jboolean 的指针。如果在一切正常,jboolean 将被设置为 JNI_FALSE,如果在执行这个函数期间的任何时候发生了异常,那么 jboolean 将被设置为 JNI_TRUE。这将给 JNU_CallMethoByName 的调用者一个明显的方法去检查是否有发生异常。

JNU_CallMethodByName 首先确保他能够创建两个本地引用:一个用于类引用,另一个用于方法调用返回的结果。接下来,它从对象获取到类引用,并查找到方法 ID。根据返回值的类型,switch 语句将调度到相应的 JNI 方法调用函数。回调返回后,如果 hasException 不为 NULL,我们调用 ExceptionCheck 来检查挂起的异常。

ExceptionCheck 方法是在 Java 2 SDK 1.2 中新加进去的。它类似于 ExceptionOccurred 函数。不同之处在于 ExceptionCheck 不会返回对异常对象的引用,但是当有挂起的异常时返回 JNI_TRUE,在没有挂起的异常的时候放回 JNI_FALSE。当本地代码仅需要知道是否发生异常但是不需要获取对异常对象的引用时,ExceptionCheck 简化了本地引用的管理。前面的代码在使用 JDK 1.1 中,将重写如下:

if (hasException) {
    jthrowable exc = (*env)->ExceptionOccurred(env);
    *hasException = exc != NULL;
    (*env)->DeleteLocalRef(env, exc);
}

额外的 DeleteLocalRef 调用时必须的,以删除对异常对象的本地引用。

使用 JNU_CallMethodByName 方法,我们可以重写 4.2 节中 InstanceMethodCall.nativeMethod 的实现:

JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) {
    printf("In C\n");
    JNU_CallMethodByName(env, NULL, obj, "callback", "()V");
}

在 JNU_CallMethodByName 调用会我们不需要检查异常,因为本地代码会立刻返回。

第七章:调用接口

这一章用于说明在你的本地代码中如何嵌入一个 Java 虚拟机。Java 虚拟机实现通过作为一个本地库来传输,本地应用程序可以连接此库并使用调用接口来加载 Java 虚拟机。的确,在 JDK 或 Java 2 SDK 版本中的标准启动器指令只不过是一个和 Java 虚拟机链接的简单 c 程序。启动器解析命令行参数、加载虚拟机、并通过调用借口运行 Java 程序。

7.1 创建 Java 虚拟机

为了说明调用借口,让我们先看一个加载一个 Java 虚拟机并调用按照如下定义的 Prog.main 方法的 C 程序

public class Prog {
    public static void main(String[] args) {
        System.out.println("Hello World " + args[0]);
    }
}

接下来的 C 程序,invoke.c,加载一个 Java 虚拟机并调用 Prog.main 方法

#include <jni.h>
#define PATH_SEPARATOR ';' /* define it to be ':' on Solaris */
#define USER_CLASSPATH "." /* where Prog.class is */
main() {
    JNIEnv *env;
    JavaVM *jvm;
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    jobjectArray args;
#ifdef JNI_VERSION_1_2
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];
    options[0].optionString = "-Djava.class.path=" USER_CLASSPATH;
    vm_args.version = 0x00010002;
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;
    /* Create the Java VM */
    res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
#else
    JDK1_1InitArgs vm_args;
    char classpath[1024];
    vm_args.version = 0x00010001;
    JNI_GetDefaultJavaVMInitArgs(&vm_args);
    /* Append USER_CLASSPATH to the default system class path */
    sprintf(classpath, "%s%c%s", vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
    vm_args.classpath = classpath;
    /* Create the Java VM */
    res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif /* JNI_VERSION_1_2 */

    if (res < 0) {
        fprintf(stderr, "Can't create Java VM\n");
        exit(1);
    }

    cls = (*env)->FindClass(env, "Prog");
    if (cls == NULL) {
        goto destroy;
    }

    mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");
    if (mid == NULL) {
        goto destroy;
    }

    jstr = (*env)->NewStringUTF(env, " from C!");
    if (jstr == NULL) {
        goto destroy;
    }

    stringClass = (*env)->FindClass(env, "java/lang/String");
    args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
    if (args == NULL) {
        goto destroy;
    }

    (*env)->CallStaticVoidMethod(env, cls, mid, args);

destroy:
    if ((*env)->ExceptionOccurred(env)) {
        (*env)->ExceptionDescribe(env);
    }
    (*jvm)->DestroyJavaVM(jvm);

上面的代码条件性的编译特定于 JDK 1.1 版本的 Java 虚拟机实现的初始化结构体 JDK1_1InitArgs。Java 2 SDK 1.2 版本中仍旧可以支持 JDK1_1InitArgs,尽管其引入了一种称为 JavaVMInitArgs 的初始化结构体。常量 JAVA_VERSION_1_2 在 Java 2 SDK 1.2 版本中定义,而在 JDK 1.1 版本中是没有定义的。

当它针对的是 1.1 版本时,C 代码通过调用 JNI_GetDefaultJavaVMInitArgs 来获取默认的虚拟机设置。JNI_GetDefaultJavaVMInitArgs 在 vm_args 参数中返回诸如堆大小、栈大小、默认类路径等值。然后我们追加 Prog.class 所在的目录到 vm_args.classpath 中。

当它是针对的是 1.2 版本时,C 代码创建一个 JavaVMInitArgs 结构体。虚拟机初始化结构体保存在 JavaVMOption 数组当中,你可以设置和 Java 命令行选项对应的常规选项(如-Djava.class.path=.)和虚拟机实现特定的选项(如-Xmx64m)。设置 ignoreUnrecognized 字段为 JNI_TRUE 说明虚拟机忽略不能识别的虚拟机特定选项。

在设置好虚拟机初始化结构体后,C 程序调用 JNI_CreateJavaVM 来加载和初始化 Java 虚拟机。JNI_CreateJavaVM 将填充两个返回值:

  • jvm,指向新创建的 Java 虚拟机的接口指针
  • env,指向当前线程的 JNIEnv 的接口指针,本地方法将通过 env 接口指针来调用 JNI 方法    当 JNI_CreateJavaVM 成功返回时,当前本地线程就已经将自身引导到 Java 虚拟机中。在这个点上,它就像一个本地方法一样运行,因此,除了别的以外,它可以发出 JNI 调用来调用 Prog.main 方法。

最终程序会调用 DestroyJavaVM 来卸载 Java 虚拟机。(不幸的是,你不能在 JDK 1.1 版本和 Java 2 SDK 1.2 版本中卸载 Java 虚拟机实现,在这些版本中,DestroyJavaVM 总是返回错误代码。)运行上面的程序,将得到:

Hello World from C!

7.2 将本地应用程序与 Java 虚拟机相连

调用接口需要你将你的程序例如 invoke.c,和一个 Java 虚拟机实现相连。如何和 Java 虚拟机相连取决于本地引用程序是仅部署在特定的 Java 虚拟机实现还是它被设计为与来自不同供应商的各种虚拟机实现一起工作。

7.2.1 与已知的 Java 虚拟机链接

你可能决定了你的程序仅部署在特定的 Java 虚拟机实现上。在这种情况下,你可以将本地应用程序和实现了虚拟机的本地库相连,例如在 Solaris 系统,JDK 1.1 版本中,你可以使用下面的指令来编译和链接 invoke.c:

cc -I<jni.h dir> -L<libjava.so dir> -lthread -ljava invoke.c

-lthread 选项表明我们使用 Java 虚拟机以及本地线程支持(8.1.5 节)。-ljava 选项表明 libjava.so 是实现了 Java 虚拟机的 Solaris 共享库。

在 Win32 系统使用 Microsoft Visual C++编译器,编译和链接同一个程序的命令行为

cl -I<jni.h dir> -MD invoke.c -link <javai.lib dir>\javai.lib

当然你需要提供与机器上的 JDK 安装相对应的正确的包含和库目录。-MD 选项确保你的本地应用程序和 Win32 多线程 C 库链接,在 JDK 1.1 版本和 Java 2 JDK 1.2 版本的 Java 虚拟机实现使用的是同一个本地 C 库。cl 命令参考 Win32 中附带的 JDK 版本 1.1 中的 javai.lib 文件,以获取有关在虚拟机中实现的调用接口函数(JNI_CreateJavaVM)的链接信息。在运行时真实使用的 JDK 1.1 虚拟机实现被包含在一个名为 javai.dll 的单独动态链接库中。相反,在链接时和运行时都是用相同的 Solaris 动态库(.so 文件)。

在 Java 2 JDK 1.2 版本中,虚拟机库的名字已更改为 Solaris 上的 libjvm.so,以及 Win32 上的 jvm.lib 和 jvm.dll。通常不同的供应商可能会以不同的方式命名虚拟机实现。

一旦编译和链接完成,你可以从命令行运行生成的可执行文件。你可能会收到系统找不到共享库或动态链接库的错误。在 Solaris 系统上,如果错误信息显示系统找不到动态库 libjava.so(libjvm.so 在 Java 2 JDK 1.2 版本上),则需要将包含虚拟机库的目录添加到 LD_LIBRARY_PATH 变量中。在 Win32 系统上,错误信息可能表示为它找不到动态链接库 javai.dll(或者 jvm.dll 在 Java 2 JDK 1.2 版本上),如果是这种情,请将包含该 DLL 的目录添加到 PATH 环境变量中。

7.2.2 与未知的 Java 虚拟机链接

如果应用程序旨在使用来自不同供应商的虚拟机实现,则无法将本地应用程序链接到特定的虚拟机实现库。因为 JNI 没有指定实现 Java 虚拟机的本地库的名称,所以你应该准备好使用不同的 Java 虚拟机实现。例如,在 Win32 上,虚拟机在 JDK 版本 1.1 中作为 javai.dll 发布,在 Java 2 SDK 版本 1.2 中作为 jvm.dll 发布。

解决方案是使用运行时动态链接来加载应用程序所需的特定虚拟机库。虚拟机库的名称可以轻松的以应用程序特定的方式配置,例如以下 Win32 代码为虚拟机库的路径找到 JNI_CreateJavaVM 的函数入口点:

/* Win32 version */
void *JNU_FindCreateJavaVM(char *vmlibpath) {
    HINSTANCE hVM = LoadLibrary(vmlibpath);
    if (hVM == NULL) {
        return NULL;
    }
    return GetProcAddress(hVM, "JNI_CreateJavaVM");
}

LoadLibrary 和 GetProcAddreee 是在 Win32 系统上,用于动态链接的 API 函数。尽管 LoadLibrary 能够接受实现了 Java 虚拟机的本地库的名字(例如“jvm”)或者路径(例如”C:\jdk1.2\jre\bin\classic\jvm.dll”),传送本地库绝对路劲给 JNU_FindCreateJavaVM 将是最好的选择。依靠 LoadLibrary 来搜索 jvm.dll,使您的应用程序易于进行配置更改,例如 PATH 环境变量的添加。

Solaris 系统的版本是类似的:

/* Solaris version */
void *JNU_FindCreateJavaVM(char *vmlibpath) {
    void *libVM = dlopen(vmlibpath, RTLD_LAZY);
    if (libVM == NULL) {
        return NULL;
    }
    return dlsym(libVM, "JNI_CreateJavaVM");
}

dlopen 和 dlsym 函数支持在 Solaris 系统上动态链接共享库。

7.3 附加本地线程

假设你有一个多线程应用程序,例如一个用 C 写的 Web 服务器。随着 HTTP 请求的到来,服务器创建一些本地线程来同时处理 HTTP 请求。我们希望在此服务器中嵌入一个 Java 虚拟机,以便多个线程可以同时在 Java 虚拟机中执行操作,如图 7.1 所示:

服务器生成的本地方法的生命周期可能比 Java 虚拟机更短。因此,我们需要一种方式来将本地线程附加到已经运行的 Java 虚拟机,在附加的本地线程中执行 JNI 调用,然后将本地线程从 Java 虚拟机中脱离不会其他已连接的线程。接下来的示例,attach.c,说明如何使用调用接口将本地线程附加到虚拟机上,改程序是使用 Win32 线程 API 编写的。可以为 Solaris 和其他操作系统编写类似的版本:

/* Note: This program only works on Win32 */
#include <windows.h>
#include <jni.h>
JavaVM *jvm; /* The virtual machine instance */

#define PATH_SEPARATOR ';'
#define USER_CLASSPATH "." /* where Prog.class is */

void thread_fun(void *arg) {
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    jobjectArray args;
    JNIEnv *env;
    char buf[100];
    int threadNum = (int)arg;

    /* Pass NULL as the third argument */
#ifdef JNI_VERSION_1_2
    res = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
#else
    res = (*jvm)->AttachCurrentThread(jvm, &env, NULL);
#endif
    if (res < 0) {
        fprintf(stderr, "Attach failed\n");
        return;
    }

    cls = (*env)->FindClass(env, "Prog");
    if (cls == NULL) {
        goto detach;
    }

    mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");
    if (mid == NULL) {
        goto detach;
    }

    sprintf(buf, " from Thread %d", threadNum);
    jstr = (*env)->NewStringUTF(env, buf);
    if (jstr == NULL) {
        goto detach;
    }

    stringClass = (*env)->FindClass(env, "java/lang/String");
    args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
    if (args == NULL) {
        goto detach;
    }
    (*env)->CallStaticVoidMethod(env, cls, mid, args);

detach:
    if ((*env)->ExceptionOccurred(env)) {
        (*env)->ExceptionDescribe(env);
    }
    (*jvm)->DetachCurrentThread(jvm);
}

main() {
    JNIEnv *env;
    int i;
    jint res;

#ifdef JNI_VERSION_1_2
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];
    options[0].optionString = "-Djava.class.path=" USER_CLASSPATH;
    vm_args.version = 0x00010002;
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = TRUE; /* Create the Java VM */
    res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
#else
    JDK1_1InitArgs vm_args;
    char classpath[1024];
    vm_args.version = 0x00010001;
    JNI_GetDefaultJavaVMInitArgs(&vm_args);
    /* Append USER_CLASSPATH to the default system class path */
    sprintf(classpath, "%s%c%s", vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
    vm_args.classpath = classpath;
    /* Create the Java VM */
    res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif /* JNI_VERSION_1_2 */

    if (res < 0) {
        fprintf(stderr, "Can't create Java VM\n");
        exit(1);
    }
    for (i = 0; i < 5; i++)
        /* We pass the thread number to every thread */
        _beginthread(thread_fun, 0, (void *)i);
        Sleep(1000); /* wait for threads to start */
        (*jvm)->DestroyJavaVM(jvm);
}

attach.c 是 invoke.c 的变体,本地代码不是在主线程中调用 Prog.main,而是启动五个线程。一旦它启动线程,它等待他们启动然后调用 DestroyJavaVM。每个产生的线程都将自己链接到 Java 虚拟机,调用 Prog.main 方法,最后在其终止运行前将其从 Java 虚拟机中分离出来。DestroyJavaVM 将在所有五个线程终止后返回。现在我们先忽略 DestroyJavaVM 的返回值,因为这个方法在 JDK 1.1 版本和 Java 2 JDK 1.2 版本上并没有完全实现。

JNI_AttachCurrentThread 将 NULL 作为其第三个参数,Java 2 JDK 1.2 版本中引入 JNI_ThreadAttachArgs 结构体,它允许你指定其他参数,例如你要附加的线程组。JNI_ThreadAttachArgs 结构体的细节将在 13.2 节中作为 JNI_AttachCurrentThread 规范的一部分进行描述。

当程序执行 DetachCurrentThread 函数时,它将释放属于当前线程的所有本地引用。运行程序会产生以下输出:

Hello World from thread 1
Hello World from thread 0
Hello World from thread 4
Hello World from thread 2
Hello World from thread 3

输出的确切顺序可能会根据线程调度中的随机因素而变化。

第八章 附加的 JNI 特性

我们已经讨论过用于编写本地代码的和在本地程序中嵌入一个 Java 虚拟机实现的 JNI 特性。这一章我们将介绍剩余的 JNI 特性。

8.1 JNI 和线程

Java 虚拟机支持在同一地址空间中同时执行的多个控制线程。这种并发性引入了一定程度的复杂性,这个在单线程环境中是没有的。多线程可以同时访问同一个对象,同一个文件描述符(简称相同的共享资源)。

为了充分利用本节,你应该熟悉多线程编程的概念。你应该知道如何编写使用多线程以及如何同步访问共享资源的 Java 应用程序。关于 Java 编程语言中多线程编程的好的参考书籍是由 Doug Lea(Addison-Wesley,1997)编写的并发编程:设计原则与模式。

8.1.1 约束

当编写运行在多线程环境中的本地方法是,你必须记住一些约束,通过理解和使用这些约束编程,无论多少线程同时执行给定的本地方法,你的本地方法都会安全的执行。例如:

  • 一个 JNIEnv 指针仅在其相关联的线程中有效。你不能将这个指针从一个线程中传递给另一个线程,或者在多线程中缓存和使用它。Java 虚拟机在同一个线程的连续调用中传递给本地方法相同的 JNIEnv 指针,但是从不同线程中调用本地方法时传递的是不同的 JNIEnv 指针。应当避免缓存一个线程的 JNIEnv 指针并在另一个线程中使用指针的常见错误。
  • 本地引用仅在创建它的线程中有效。你不能将本地引用冲一个线程中传递到另一个线程。每当有多个线程可能使用相同引用的可能性时,应始终将本地引用转换为全局引用。 8.1.2 监控进入和退出 监视器是 Java 平台上原始的同步机制。每个对象都能动态的与监视器关联。JNI 允许你使用这些监视器来进行同步,从而实现与 Java 编程语言中的同步块相当的功能:
synchronized (obj) {
    ...     // synchronized block
}

Java 虚拟机保证线程在执行块中的任何语句之前获取与对象 obj 相关联的监视器。这样可以确保在任何给定的时间内最多只有一个线程持有监视器并在同步块内运行。当等待另一个线程退出监视器时,该线程会阻塞。

本地代码可以使用 JNI 函数在 JNI 引用上执行等效的同步。你可以使用 MonitorEnter 方法来进入监视器和使用 MonitorExit 方法来退出监视器。

if ((*env)->MonitorEnter(env, obj) != JNI_OK) {
    ... /* error handling */
}
... /* synchronized block */
if ((*env)->MonitorExit(env, obj) != JNI_OK) {
    ... /* error handling */
};

执行上面的代码,一个线程在执行同步块内的任何代码前,必须首先进入与 obj 相关联的监视器。MonitorEnter 操作将一个 jobject 作为参数并且在另一个线程已经进入了和 jobject 相关联的监视器的情况下阻塞。当当前线程没有亦红优监视器时调用 MonitorExit 会导致错误并导致引发 IllegalMonitorStateException 异常。上面的代码包含一堆匹配的 MonitorEnter 和 MonitorExit 调用,但是我们仍然需要检查可能的错误。如果底层线程实现无法分配执行监视器操作所需的资源,则操作监视器可能会失败。

MonitorEnter 和 MonitorExit 工作在 jclass、jstring 和 jarray 类型上,这些都是特殊的 jobject 引用。

请记住将 MonitorEnter 调用与适当数量的 MonitorExit 调用相匹配,特别是在处理错误和异常的代码中:

if ((*env)->MonitorEnter(env, obj) != JNI_OK) ...;
...
if ((*env)->ExceptionOccurred(env)) {
    ... /* exception handling */
    /* remember to call MonitorExit here */
    if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
}
... /* Normal execution path.
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;

调用 MonitorExit 失败的话可能会导致死锁。通过比较上面你的 C 代码片段和本节开始时编写的代码片段,你可以了解到使用 Java 编程语言进行编程比 JNI 更简便。因此最好使用 Java 编程语言表示同步结构。例如,如果静态本地方法需要进入与其定义相关联的监视器,那么你应该定义一个静态同步本地方法,而不是在本地代码中执行 JNI 级的监视器同步。

8.1.3 监视等待和通知

Java API 包含几个用于线程同步的方法。它们是 Object.wait、Object.notify 和 Object.notifyAll。JNI 没有提供类似的方法和这些方法直接对应,因为监视等待和通知操作不如监视进入和退出的性能关键。本地代码可能会使用 JNI 方法调用机制来调用 Java API 中的相应方法:

/* precomputed method IDs */
static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;
void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout) {
    (*env)->CallVoidMethod(env, object, MID_Object_wait, timeout);
}

void
JNU_MonitorNotify(JNIEnv *env, jobject object) {
    (*env)->CallVoidMethod(env, object, MID_Object_notify);
}

void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object) {
    (*env)->CallVoidMethod(env, object, MID_Object_notifyAll);
}

我们假设 Object.wait、Object.notify 和 Object.notifyAll 的方法 ID 已经在其他地方计算过,并且被缓存在全局变量中。就像 Java 编程语言一样,只有在持有与 Object 参数相关量的监视器时,才能调用上述的监视器相关的功能。

8.1.4 在任意上下文中获取一个 JNIEnv 指针

之前我们就已经解释过,一个 JNIEnv 指针仅在与其相关联的线程中有效。对于本地方法,这通常不是问题,因为他们从虚拟机接受 JNIEnv 指针作为第一个参数。然而有时候可能不需要直接从虚拟机调用的本地代码来获取属于当前线程的 JNIEnv 接口指针。例如,属于“callback”的一端本地代码可能会被操作系统调用,在这种情况下,JNIEnv 指针可能不能用作参数。你可以通过调用 AttachCurrentThread 方法来获取当前线程的 JNIEnv 指针:

JavaVM *jvm; /* already set */
f() {
    JNIEnv *env;
    (*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
    ... /* use env */
}

当当前线程已经附加到虚拟机上时,AttachCurrentThread 返回属于当前线程的 JNIEnv 接口指针。

有许多中方式来获取 JavaVM 指针:通过在创建虚拟机的时候记录它、通过使用 JNI_GetCreatedJavaVMs 来查询已经创建了的虚拟机、通过在常规方法内调用 GetJavaVM、或通过定义 JNI_OnLoad 处理程序来记录虚拟机,与 JNIEnv 指针不同,JavaVM 指针在多个进行之间保持有效,因此可以缓存在全局变量中。

Java 2 SDK 1.2 版本提供了一个新的调用接口方法 GetEnv,因此你可以使用它来检查当前线程是否已经附加到一个虚拟机上,并且如果已经附加到虚拟机上了,它会返回属于当前线程的 JNIEnv 指针。

8.1.5 线程模型匹配

假设本地代码在多个线程中执行并访问同一个全局引用。本地代码应该按使用 JNI 函数 MonitorEnter 和 MonitorExit 还是使用主机环境中的本地线程同步原语(例如 Solaris 上的 mutex_lock)?同样如果本地代码需要创建一个新的线程,那么应该创建一个 java.lang.Thread 对象并通过 JNI 调用 Thread.start 还是应该在主机环境中使用本地线程创建原语(例如 Solaris 上的 thr_create)?

答案是,如果 Java 虚拟机实现支持与本地代码使用的线程模型匹配的线程模型,则所有这些方法都可以工作。线程模型指示着系统如何实现基本的线程操作,例如系统调度、上下文切换、同步和阻塞。在本地线程中模型中,操作系统管理所有这些必须的线程操作。另一方面,在一个用户线程模型中,应用程序代码实现线程操作。例如,Solaris 上附带的 JDK 和 Java 2 SDK 版本中的“Green thread”模型使用 ANSI C 函数 setjmp 和 longjmp 来实现上下文切换。

许多现代操作系统(例如 Solaris 和 Win32)支持本地线程模型。不幸的是,一些操作系统仍然缺少本地线程支持。相反,这些操作系统上可能有一个或多个用户线程包。

如果你严格使用 Java 编程语言编写应用程序,则无需担心虚拟机实现的底层线程模型。Java 平台能够移植到任何支持所需线程原语的主机环境中。大多数本地和用户线程包提供了用于实现 Java 虚拟机必须的线程原语。

另一方面,JNI 程序员必须注意线程模型。如果 Java 虚拟机实现和本地代码具有不同的线程和同步概念,那么使用本地代码的应用程序可能就无法正常运行。例如,本地方法可能会在自己的线程模型中执行同步操作时会阻塞,但是运行不同线程模型的 Java 虚拟机可能不会意识到执行本地方法的线程被阻塞。应用程序死锁是因为其他线程无法被调用。如果本地代码和 Java 虚拟机实现使用相同的线程模型,则它们的线程模型匹配。如果 Java 虚拟机实现支持本地线程,那么本地代码就能够在主机环境中自由的调用线程相关的原语。如果 Java 虚拟机实现是基于一个用户线程包的话,那么本地代码也应该链接到相同的用户线程包或者不要依赖多线程操作。后者可能比你想象的更难实现:大多数 C 库调用(如 I/O 何内存分配功能)执行底层线程同步。除非本地代码进行纯粹的计算并且不适用库调用,否则可能会间接使用线程原语。

大多数虚拟机实现为 JNI 本地代码,仅支持特定的线程模型。支持原生线程的虚拟机实现是最灵活的,因此本地线程,当可用时,应当是主机环境的首选。依赖于特性用户线程程序包的虚拟机实现可能严重受限于他们可以操作的本机代码的类型。

一些虚拟机实现可能支持许多不同的线程模型。更灵活的虚拟机实现类型甚至可以允许你为虚拟机内部使用提供自定义的线程模型实现,从而确保虚拟机实现可以和你的本地代码一同工作。在开始需要本地代码的项目前,你应该查看虚拟机实现附带的文档,以了解线程模型的限制。

8.2 编写国际化的代码

当编写在多个国家地区运行良好的代码时需要特别注意。JNI 使程序员可以完全访问 Java 平台的国际化特性。我们使用字符装换作为例子,因为在许多语言环境中,文件名和信息可能包含许多非 ASCII 字符。

Java 虚拟机使用 Unicode 格式来表示字符串。虽然一些本地平台(例如 Windows NT)同样支持 Unicode,但是还是使用特定于当地语言环境的编码来代表字符串。

不要使用 GetStringUTFChars 和 GetStringUTFRegion 函数在 jstrings 和特定于语言环境的字符串之间进行转换,除非 UTF-8 碰巧是平台的本地编码格式。当表示名称和描述符(例如 GetMethodID 的参数)时,UTF-8 字符串非常有用,它将传递给 JNI 函数,但是却不适用于特定语言环境的字符串,如文件名。

8.2.1 从本地字符串中创建 jstrings

使用 String(byte[] bytes)构造器将一个本地字符串转换成 jstring。接下来的辅助函数从一个本地语言环境编码的 C 字符串中创建一个 jstring

jstring JNU_NewStringNative(JNIEnv *env, const char *str) {
    jstring result;
    jbyteArray bytes = 0;
    int len;
    if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
        return NULL; /* out of memory error */
    }
    len = strlen(str);
    bytes = (*env)->NewByteArray(env, len);
    if (bytes != NULL) {
        (*env)->SetByteArrayRegion(env, bytes, 0, len, (jbyte *)str);
        result = (*env)->NewObject(env, Class_java_lang_String, MID_String_init, bytes);
        (*env)->DeleteLocalRef(env, bytes);
        return result;
    } /* else fall through */
    return NULL;
}

这个方法创建创建一个 byte 数组、复制本地 C 字符串到 byte 数组中并且最后调用 String(byte[] bytes)的构造函数创建最后的 jstring 对象。Class_java_lang_String 是一个指向 java.lang.String 的全局引用,MID_String_init 是 string 构造函数的方法 ID。因为这是要一个辅助函数,我们需要确保删除掉临时创建来保存字符的本地应用 byte 数组。

如果你需要在 JDK 版本 1.1 中使用此函数,需要删除对 EnsureLocalCapacity 的调用。

```c8.2.2 将 jstrings 转换成本地字符串

使用 String.getBytes 方法将一个 jstring 字符串转换成恰当的本地编码。下面的辅助函数将一个 jstring 字符串转换成本地环境特定的本地字符串:

char *JNU_GetStringNativeChars(JNIEnv *env, jstring jstr)
{
    jbyteArray bytes = 0;
    jthrowable exc;
    char *result = 0;
    if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
        return 0; /* out of memory error */
    }
    bytes = (*env)->CallObjectMethod(env, jstr, MID_String_getBytes);
    exc = (*env)->ExceptionOccurred(env);
    if (!exc) {
        jint len = (*env)->GetArrayLength(env, bytes);
        result = (char *)malloc(len + 1);
        if (result == 0) {
            JNU_ThrowByName(env, "java/lang/OutOfMemoryError", 0);
            (*env)->DeleteLocalRef(env, bytes);
            return 0;
        }
        (*env)->GetByteArrayRegion(env, bytes, 0, len, (jbyte *)result);
        result[len] = 0; /* NULL-terminate */
    } else {
        (*env)->DeleteLocalRef(env, exc);
    }
    (*env)->DeleteLocalRef(env, bytes);
    return result;
}

这个方法将 java.lang.String 引用传递给 String.getBytes 方法,然后将 byte 数组的元素复制到一个新分配的 C 数组中。MID_String_getBytes 是预先计算好的 String.getbytes 的方法 ID。因为这是一个辅助函数,我们需要确保 byte 数组的本地引用被删除并且处理异常对象。需要记住的是,删除引用异常对象的 JNI 引用不会清除挂起的异常。

再次强调,如果你需要在 JDK 版本 1.1 中使用此函数,需要删除对 EnsureLocalCapacity 的调用。

8.3 注册本地方法

在一个应用程序执行一个本地方法前,它需要执行两个步骤来加载包含本地代码实现的本地库,然后再链接到本地方法实现。

  • System.loadLibrary 定位和加载命名的本地库。例如,在 Win32 系统中 System.loadLibrary(“foo”)会是 foo.dll 被加载。
  • 虚拟机会在加载的本地库中定位到本地方法实现。例如,一个 Foo.g 方法调用需要定位和链接到可能存在于 foo.dll 库中的本地方法 Java_Foo_g。    这一节会介绍另一个方法来完成第二个步骤。JNI 程序员能够使用一个类的引用、方法名字和方法描述符来注册方法指针的方法手动的连接本地库,而不是依赖虚拟机在已加载的本地库中查找本地方法:
JNINativeMethod nm;
nm.name = "g";
/* method descriptor assigned to signature field */
nm.signature = "()V";
nm.fnPtr = g_impl;
(*env)->RegisterNatives(env, cls, &nm, 1);

上面的代码注册一个本地方法 g_impl 作为 Foo.g 方法的本地实现:

void JNICALL g_impl(JNIEnv *env, jobject self);

本地方法 g_impl 不需要遵循 JNI 的命名规则,因为只涉及函数指针调用,也不需要从库中导出(因此不需要使用 JNIEXPORT 声明方法)。但是,本地 g_impl 方法仍然需要遵循 JNICALL 调用规则。

RegisterNatives 方法有很多用途:

  • 注册大量本地方法实现时更加方便和有效,而不是让虚拟机懒散的连接这些方法的入口点。
  • 你可以在一个方法中多次调用 RegisterNatives 方法,允许本地方法实现在运行时被更新。
  • 当一个本地应用程序需要嵌入一个虚拟机实现,并且需要链接到定义在该本地应用程序中的地方法时,RegisterNatives 将非常有用。虚拟机不能够自动查找这个本地方法引用,因为它只能够在本地库中查找而不是应用程序中。

8.4 加载和卸载处理程序

加载和卸载处理程序允许本地库导出两个方法:其中一个当 System.loadLibrary 加载本地库是会被调用,另一个当虚拟机卸载本地库是会被调用。这个特性是在 Java 2 SDK 1.2 版本中加入的。

8.4.1 JNI_OnLoad 处理程序

当 System.loadlibrary 加载一个本地库时,虚拟机会在本地库中查找下述的导出的程序入口:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved);

在一个 JNI_OnLoad 实现内,你可以调用任何的 JNI 方法。一个 JNI_OnLoad 处理程序的典型用法是缓冲 JavaVM 指针、类引用或者方法和字段 ID,如下面展示的例子一样:

JavaVM *cached_jvm;
jclass Class_C;
jmethodID MID_C_g;
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved)
{
    JNIEnv *env;
    jclass cls;
    cached_jvm = jvm; /* cache the JavaVM pointer */
    if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_2)) {
        return JNI_ERR; /* JNI version not supported */
    }
    cls = (*env)->FindClass(env, "C");
    if (cls == NULL) {
        return JNI_ERR;
    }
    /* Use weak global ref to allow C class to be unloaded */
    Class_C = (*env)->NewWeakGlobalRef(env, cls);
    if (Class_C == NULL) {
        return JNI_ERR;
    }
    /* Compute and cache the method ID */
    MID_C_g = (*env)->GetMethodID(env, cls, "g", "()V");
    if (MID_C_g == NULL) {
        return JNI_ERR;
    }
    return JNI_VERSION_1_2;
}

JNI_OnLoad 方法首先缓存 JavaVM 指针在全局变量 cached_jvm 中。然后通过调用 GetEnvironment 获取 JNIEnv 指针。最后加载 C 类,或者类引用并计算出 C.g 的方法 ID。JNI_OnLoad 方法返回 JNI_ERR(12.4 节)作为错误指示,否则返回本地库需要的 JNIEnv 的版本号 JNI_VERSION_1_2。

我们会在下一节中解释为什么我们在一个弱全局引用中缓存 C 类而不是在一个全局引用中缓存 C 类。

给定了一个缓存的 JavaVM 接口指针,那么实现一个允许本地代码获取当前线程的 JNIEnv 接口指针的辅助函数是非常简便的。

JNIEnv *JNU_GetEnv() {
    JNIEnv *env;
    (*cached_jvm)->GetEnv(cached_jvm, (void **)&env, JNI_VERSION_1_2);
    return env;
}

8.4.2 JNI_OnUnload 处理程序

直观上,当虚拟机卸载一个本地库的时候,它会调用 JNI_OnUnload 处理程序。然而这还不够精确。虚拟机什么时候能够确认他可以卸载一个本地库?哪一个线程执行 JNI_OnUnload 处理程序?

卸载本地库的规则如下:

  • 虚拟机将每一个本地库与发出 System.loadLibrary 调用的类 C 的类加载器 L 相关联
  • 在虚拟机确认 L 加载器再也没有存活的对象时,它将调用 JNI_OnUnload 处理程序并卸载本地库。因为类加载器引用了它所定义的所有类,所以这意味着 C 也可以被卸载。
  • JNI_OnUnload 处理程序在终结器中运行,可以有 java.lang.runFinalization 同步调用,也可以由虚拟机异步调用。    下面是一个 JNI_OnUnload 处理程序的定义,它清楚了上一节 JNI_OnLoad 处理程序申请的资源
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *jvm, void *reserved) {
    JNIEnv *env;
    if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_2)) {
        return;
    }
    (*env)->DeleteWeakGlobalRef(env, Class_C);
    return;
}

JNI_OnUnload 方法删除在 JNI_OnLoad 处理程序中创建的 C 类的全局弱引用。我们不需要删除 MID_C_g 的方法 ID,因为虚拟机在卸载其定义的 C 类时会自动回收表示 C 类的方法 ID 所占用的资源。

现在我们准备解释为什么我们在一个弱全局引用中缓存 C 类,而不是在全局引用中缓存 C 类。一个全局引用会一直保持 C 是存活的,这样同时也会保证 C 类加载器存活。给定的本地库是和 C 类的加载器 L 相关联的,所以本地库将不会被卸载而且 JNI_OnUnload 将不会被调用。

JNI_UnLoad 处理程序在终结器中运行。相反 JNI_OnLoad 处理程序在启动 System.loadLibrary 调用的线程中运行。因为 JNI_OnUnload 运行在一个未知的线程上下文中,为了避免可能的死锁,你在 JNI_OnUnload 中应该避免复杂的同步和加锁操作。JNI_OnUnload 通常执行简单的任务,例如释放本地库申请的资源。当加载库的类加载器并且由该类加载器定义的类不再存活时,JNI_OnUnload 处理程序就会运行。JNI_OnUnload 处理程序将不得以任何方法使用这些类。在上面的 JNI_OnUnload 定义中,你不得执行任何假定 Class_C 仍引用有效类的操作。示例中的 DeleteWeakGlobalRef 调用为弱全局引用本身释放内存,但是不以任何方式操作类 C。

总之,当调用 JNI_OnUnload 处理程序的时候,你应该非常消息,避免复杂的锁定操作引起死锁。记住,当 JNI_OnUnload 处理程序被调用时,类已经被卸载了。

8.5 反射支持

反射通常是指运行时操作语言级别的结构。例如,反射机制能够让你在运行时去发现任意类对象的名字以及定义在类中的一些列的字段和方法。Java 编程语言通过 java.lang.reflect 包以及 java.lang.Object 和 java.lang.Class 类中的一些方法来支持反射机制。尽管你能够经常调用相应的 Java API 来进行反射操作,JNI 提供以下方法,使本地代码频繁的反射操作更加高效和方便:

  • GetSuperClass 返回一个给定类引用的超类。
  • IsAssignableFrom 用于检查在另一个类的实例产生预期效果时,一个类的实例是否可以使用。
  • GetObjectClass 返回给定对象引用的类
  • IsInstanceOf 检查一个 jobject 引用是否是一个给定类的实例。
  • FromReflectedField 和 ToReflectedField 允许本地代码在字段 ID 和 jva.lang.reflect.Field 对象之间转换。这个是在 Java 2 SDK 1.2 版本上新添加的。
  • FromReflectedMethod 和 ToReflectedMethod 允许本地代码在方法 ID、java.lang.reflect.Method 对象和 java.lang.reflect.Constructor 对象之间转换。这个是在 Java 2 SDK 1.2 版本上新添加的。

8.6 使用 C++进行 JNI 编程

JNI 为 C++程序员提供了一个稍微简单的接口,jni.h 文件中包含一组定义,以便 C++程序员编写,例如:

jclass cls = env->FindClass("java/lang/String");

而在 C 语言中:

jclass cls = (*env)->FindClass(env, "java/lang/String");

env 上间接寻址的额外级别以及 FindClass 的 env 参数对程序员是隐藏的。C++编译器将 C++成员函数内联到相应的 C 对象上,结果代码是完全一样的。在 C 和 C++中使用 JNI 没有内在的性能差异。另外,jni.h 文件还定义了一组虚拟的 C++类来强制不同的 jobject 子类型之间的子类型关系:

// JNI reference types defined in C++
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
...
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
...

C++编译器能够在编译时检查是否通过,例如将一个 jobject 传递到 GetMethodID 中:

// ERROR: pass jobject as a jclass:
jobject obj = env->NewObject(...);
jmethodID mid = env->GetMethodID(obj, "foo", "()V");

因为 GetMethodID 期望一个 jclass 引用,C++编译器会给出错误的信息。在 JNI 的 C 类型定义中,jclass 和 jobject 是等同的:

typedef jobject jclass;

因此,C 编译器无法检测到您错误地传递了 jobject 而不是 jclass。

&ems; C++中添加的类型层次有时需要额外的投射。在 C 中,你可以从一个字符串数组中获取一个字符串,并将结果赋给一个 jstring:

jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);

但是在 C++中,你需要插入一个显示的转换:

jstring jstr = (jstring)env->GetObjectArrayElement(arr, i);