欢迎关注大数据技术架构与案例微信公众号:过往记忆大数据
过往记忆博客公众号iteblog_hadoop
欢迎关注微信公众号:
过往记忆大数据

《深入理解 JNI》JNI 高级特性

本文是《深入理解 JNI》专题的第 5 篇,共 5 篇:

全局引用与弱全局引用

在JNI编程中,管理对象引用的生命周期是非常重要的。JNI提供了几种不同类型的引用,以适应不同的使用场景。其中,全局引用(Global Reference)和弱全局引用(Weak Global Reference)是两种常用的引用类型。

全局引用(Global Reference)

全局引用是JNI中最强的引用类型。一旦一个Java对象被全局引用指向,它就不会被垃圾收集器回收,即使所有其他的引用都已经超出作用域。全局引用通常用于以下场景:

  • 当本地代码需要在多个方法调用之间保持对Java对象的引用时。
  • 当本地代码需要将Java对象传递给其他线程时。

创建全局引用的函数是`JNIEnv`的`NewGlobalRef`。使用全局引用时需要注意,因为它们会阻止对象被垃圾回收,所以必须谨慎管理,避免内存泄漏。

示例代码:创建和使用全局引用

JNIEXPORT void JNICALL Java_ClassName_createGlobalRef(JNIEnv *env, jobject obj) {
    // 创建全局引用
    jobject globalRef = (*env)->NewGlobalRef(env, obj);
    if (globalRef == NULL) {
        return; // 创建失败
    }

    // ... 使用全局引用 ...

    // 使用完毕后删除全局引用
    (*env)->DeleteGlobalRef(env, globalRef);
}

弱全局引用(Weak Global Reference)

弱全局引用是一种较弱的全局引用,它允许垃圾收集器回收被引用的Java对象。弱全局引用通常用于以下场景:

  • 当本地代码需要长期保持对Java对象的引用,但又不想阻止对象被垃圾回收时。
  • 当需要缓存对象引用,但又希望缓存不会阻止对象被回收时。

创建弱全局引用的函数是`JNIEnv`的`NewWeakGlobalRef`。使用弱全局引用时,需要注意检查对象是否已经被回收。

示例代码:创建和使用弱全局引用

JNIEXPORT void JNICALL Java_ClassName_createWeakGlobalRef(JNIEnv *env, jobject obj) {
    // 创建弱全局引用
    jobject weakGlobalRef = (*env)->NewWeakGlobalRef(env, obj);
    if (weakGlobalRef == NULL) {
        return; // 创建失败
    }

    // ... 使用弱全局引用 ...

    // 检查对象是否已被回收
    jobject ref = (*env)->NewLocalRef(env, weakGlobalRef);
    if (ref != NULL) {
        // 对象仍然存在
        (*env)->DeleteLocalRef(env, ref);
    } else {
        // 对象已被回收
    }

    // 删除弱全局引用
    (*env)->DeleteWeakGlobalRef(env, weakGlobalRef);
}

注意事项

  • 全局引用和弱全局引用都需要手动删除,以避免内存泄漏。
  • 使用全局引用时要特别小心,确保不会因为忘记删除而导致内存泄漏。
  • 弱全局引用虽然允许对象被垃圾回收,但在对象被回收后,相关的弱全局引用会自动失效,需要重新获取对象引用。

通过使用全局引用和弱全局引用,JNI提供了一种灵活的方式来管理Java对象的生命周期,使得本地代码可以在需要时保持对对象的引用,同时又不阻碍垃圾收集器的正常工作。

异常处理

在JNI编程中,处理异常是一个重要的环节。Java代码可能会抛出异常,而这些异常需要在本地代码中进行适当的处理。JNI提供了一组函数来检查和处理Java异常。

检查异常

当Java方法调用发生异常时,相关的JNI函数会返回错误代码,并且可以通过`JNIEnv`指针的`ExceptionOccurred`函数来检查是否有异常发生。

jthrowable exception = (*env)->ExceptionOccurred(env);
if (exception != NULL) {
    // 异常发生
}

清除异常

如果检测到异常,可以使用`ExceptionClear`函数来清除异常状态。这通常在处理完异常后进行,以便JNI调用链可以继续正常执行。

(*env)->ExceptionClear(env);

抛出异常

JNI允许本地代码抛出Java异常。可以使用`ThrowNew`函数来抛出一个新的异常。

jclass exceptionClass = (*env)->FindClass(env, "java/lang/RuntimeException");
if (exceptionClass != NULL) {
    (*env)->ThrowNew(env, exceptionClass, "An error occurred in native code");
}

示例代码:JNI中的异常处理

以下是一个简单的JNI函数示例,展示了如何检查和处理Java异常:

JNIEXPORT void JNICALL Java_ClassName_nativeMethod(JNIEnv *env, jobject obj) {
    // ... 执行一些操作 ...

    // 检查是否有异常发生
    if ((*env)->ExceptionOccurred(env)) {
        // 处理异常
        (*env)->ExceptionDescribe(env); // 打印异常信息到标准错误输出
        (*env)->ExceptionClear(env); // 清除异常状态

        // 可以选择抛出自定义异常或执行其他错误处理逻辑
        jclass exceptionClass = (*env)->FindClass(env, "java/lang/RuntimeException");
        if (exceptionClass != NULL) {
            (*env)->ThrowNew(env, exceptionClass, "An error occurred in native code");
        }
    }

    // ... 继续执行其他操作 ...
}

在本地代码中处理异常时需要注意以下几点:

  • 不要忽略异常。如果Java代码抛出异常,本地代码应该检查并适当地处理它。
  • 清除异常后,应该谨慎地决定程序的下一步行动,因为异常可能会导致对象处于不一致的状态。
  • 抛出异常时,确保提供有意义的错误信息,以便Java层的代码可以理解和处理这些异常。

通过JNI提供的异常处理机制,本地代码可以与Java层的异常处理逻辑协同工作,确保程序的健壮性和稳定性。
以下是对第五部分第三小节“多线程与JNI”内容的扩充:

多线程与JNI

多线程支持概述

  • JNI中的多线程模型
    • 在JNI环境下,Java虚拟机(JVM)本身是支持多线程运行的。这意味着可以有多个Java线程同时存在并执行。然而,对于本地代码(C/C++ 等)与JVM之间的交互,在多线程场景下有特殊的规则和要求。
    • JVM的多线程模型基于操作系统的线程实现,例如在类Unix系统中通常基于pthread库。这种基于底层操作系统线程构建的方式使得JVM能够充分利用多核处理器的性能优势。
  • 线程创建与管理
    • 在JNI环境中创建线程,可以使用多种方式。如果是在Android平台上,可以利用Android提供的线程创建机制,如`pthread_create`函数(遵循POSIX线程标准)。在其他平台或者更通用的场景下,也可以使用C++11中的`std::thread`库来创建线程。
    • 当创建一个新线程时,需要考虑线程的生命周期管理。这包括线程启动时的初始化工作,例如设置线程特定的数据(如果需要),以及在线程结束时进行资源的清理和释放。对于JNI相关的线程,特别要注意与JVM的正确交互,确保不会出现资源泄漏或者未定义的行为。

AttachCurrentThread与DetachCurrentThread

  • AttachCurrentThread
    • `AttachCurrentThread`函数是JNI中用于将当前本地线程附着到JVM的关键函数。它的作用是让JVM识别这个本地线程,从而使得该线程能够调用JNI函数。
    • 在调用`AttachCurrentThread`时,需要传递一个指向`JNIEnv`指针的指针(通常为`JNIEnv **`类型)以及一些可选的线程属性参数。JVM会为这个线程分配一个`JNIEnv`实例,并将其地址通过传入的指针返回。
    • 例如,在C代码中可能的调用方式如下:
JavaVM *jvm;
JNIEnv *env;
// 假设jvm已经被正确初始化
(*jvm)->AttachCurrentThread(jvm, &env, NULL);
  • 这个函数必须在本地线程首次调用任何JNI函数之前被调用,否则会导致未定义的行为。
    • DetachCurrentThread
    • 当本地线程完成与JVM相关的操作后,必须调用`DetachCurrentThread`函数将线程从JVM中分离。这是因为JVM会为附着线程维护一些内部状态信息,如果不进行分离,可能会导致内存泄漏等问题。
    • 分离线程的操作相对简单,只需要传入对应的`JavaVM`指针即可。例如:
(*jvm)->DetachCurrentThread(jvm);

一旦线程被分离,就不能再在该线程中调用JNI函数,除非再次调用`AttachCurrentThread`重新附着。

线程局部存储与JNIEnv

  • 线程局部存储(TLS)
    • JNI使用线程局部存储来管理每个线程的`JNIEnv`指针。这意味着每个线程都有自己独立的`JNIEnv`实例,它们之间互不干扰。
    • TLS是一种机制,允许每个线程拥有其自己的数据副本。在JNI中,这种机制确保了当多个线程同时运行时,不会出现`JNIEnv`指针的混淆或者冲突。
    • 从实现角度来看,在底层操作系统或者JVM内部,通过特定的数据结构和算法来维护每个线程与它的`JNIEnv`指针之间的映射关系。
  • JNIEnv的使用限制
    • 强调`JNIEnv`指针具有很强的线程局限性,它只能在创建它的线程中使用。这是因为`JNIEnv`指针指向的是与特定线程相关的JVM内部数据结构和函数表。
    • 如果试图将一个线程的`JNIEnv`指针传递给另一个线程并使用,会导致不可预测的结果,如程序崩溃或者数据损坏。
  • 同步与线程安全

    • 同步机制
      • 在多线程环境中,为了确保数据的一致性和正确性,需要使用同步机制。在JNI编程中,可以利用Java层的`synchronized`关键字来实现对象级别的同步。例如,在Java代码中对共享对象的方法进行同步声明,在本地代码中调用这些同步方法时,就会遵循Java的同步规则。
      • 同时,在C/C++ 层面,可以使用互斥锁(mutex)来实现更细粒度的同步控制。例如,在访问共享资源(如全局变量或者文件句柄)之前,先获取互斥锁,访问结束后释放锁。这样可以防止多个线程同时访问共享资源导致的竞态条件。
      • 除了互斥锁,还可以使用条件变量(condition variable)来实现线程间的通信和协调。例如,一个线程可以等待某个条件满足(通过条件变量),而另一个线程在满足条件后通知等待的线程继续执行。
    • 线程安全
      • 在JNI编程中确保线程安全是一个综合性的任务。除了正确使用同步机制外,还需要注意数据的初始化和清理顺序。例如,在多个线程中共享的Java对象或者本地数据结构,要确保在所有线程开始访问之前已经正确初始化,并且在所有线程都不再使用之后进行彻底的清理。
      • 另外,对于JNI函数本身的调用也要遵循线程安全的规则。有些JNI函数可能在内部有特定的线程假设或者限制,需要仔细阅读JNI文档并正确使用。

    示例代码

    • 多线程示例
      • 以下是一个简单的JNI多线程示例,展示了如何创建线程、附着到JVM、调用JNI函数以及分离线程。
    #include <jni.h>
    #include <pthread.h>
    
    JavaVM *jvm;
    
    void *nativeThreadFunc(void *arg) {
        JNIEnv *env;
        // 附着线程到JVM
        (*jvm)->AttachCurrentThread(jvm, &env, NULL);
    
        // 这里可以进行JNI相关的操作,例如调用Java方法或者访问Java对象
        // 假设已经有一个Java类的引用jclass cls和一个方法ID mid
        jobject obj = // 获取Java对象的方式
        (*env)->CallVoidMethod(env, obj, mid);
    
        // 分离线程
        (*jvm)->DetachCurrentThread(jvm);
        return NULL;
    }
    
    JNIEXPORT void JNICALL Java_ClassName_nativeMethod(JNIEnv *env, jobject thiz) {
        // 获取JavaVM指针
        (*env)->GetJavaVM(env, &jvm);
    
        pthread_t thread;
        // 创建线程
        pthread_create(&thread, NULL, nativeThreadFunc, NULL);
        // 等待线程结束(这里可以根据实际需求决定是否等待)
        pthread_join(thread, NULL);
    }
    
    本博客文章除特别声明,全部都是原创!
    原创文章版权归过往记忆大数据(过往记忆)所有,未经许可不得转载。
    本文链接: 【《深入理解 JNI》JNI 高级特性】(https://www.iteblog.com/archives/10224.html)
    喜欢 (0)
    分享 (0)
    发表我的评论
    取消评论

    表情
    本博客评论系统带有自动识别垃圾评论功能,请写一些有意义的评论,谢谢!