【问题标题】:In JNI, how do I cache the class, methodID, and fieldIDs per IBM's performance recommendations?在 JNI 中,如何根据 IBM 的性能建议缓存类、方法 ID 和字段 ID?
【发布时间】:2012-05-23 23:14:16
【问题描述】:

我在on IBM 上看到

要访问 Java 对象的字段并调用它们的方法,本机代码 必须调用 FindClass()、GetFieldID()、GetMethodId() 和 获取静态方法 ID()。在 GetFieldID()、GetMethodID() 和 GetStaticMethodID(),为给定类返回的 ID 不会改变 在 JVM 进程的生命周期内。但是获取字段的调用或 方法可能需要在 JVM 中进行大量工作,因为字段和 方法可能是从超类继承的,使得 JVM 沿着类层次结构查找它们。因为ID是一样的 对于给定的类,您应该查找它们一次,然后重用它们。 同样,查找类对象可能很昂贵,因此它们应该 也被缓存。

如何缓存 JNI 中的methodIDfieldIDclass 对象? 是否有必须遵循的内置方法或特定程序?

【问题讨论】:

    标签: java performance caching java-native-interface


    【解决方案1】:

    没有可遵循的内置方法,但这里有一个标准、干净且可重复的实现,展示了我如何实践 IBM 的建议。

    我将假设您从 Java 调用 DLL,并且在整个应用程序生命周期中多次引用它。

    示例 Native Java 类被命名为 org.stackoverflow.jni.NativeClazz,它将实现两个内置的 JNI 方法 JNI_OnLoad()JNI_OnUnload()

    void JNI_OnLoad(JavaVM *vm, void *reserved):该方法将用于将类 ID 注册为全局变量,并将方法 ID 和字段 ID 分配给静态变量。该方法在Java VM加载驱动时自动调用;它在驱动程序生命周期中只被调用一次。

    void JNI_OnUnload(JavaVM *vm, void *reserved):此方法将用于释放JNI_OnLoad() 注册的所有全局变量。 VM 将在应用程序关闭前立即自动调用JNI_OnUnload()

    理由:据我了解,必须将类 ID 注册为全局引用,以保持任何关联的方法 ID/字段 ID 的可行性。如果没有这样做并且类从 JVM 中卸载,则在类重新加载时,方法 ID/字段 ID 可能不同。如果 Class ID 注册为全局引用,则关联的 Method IDs 和 Field IDs 不需要注册为全局引用。将类 ID 注册为全局引用可防止卸载关联的 Java 类,从而稳定方法 ID/字段 ID 值。应在 JNI_OnUnload() 中删除全局引用,包括类 ID。

    方法 ID 和字段 ID 不由本机代码管理;它们由虚拟机管理并且在关联类被卸载之前一直有效。在虚拟机卸载定义类之前,不能显式删除字段 ID 和方法 ID;它们可以在卸载后留给 VM 处理。

    示例代码

    以下 C++ 代码部分中的注释解释了全局注册变量。

    这是代表数据对象的 Java 类BeanObject

    package org.stackoverflow.data;
    
    public class BeanObject {
    
        String foo = "";
        
        public String getFoo() {
         
            return foo;
        }
    }
    

    这是一个骨架 Java 类NativeClazz

    package org.stackoverflow.jni;
    
    import org.stackoverflow.data.BeanObject;
    
    public class NativeClazz {
    
        // Static area for forced initialization
        static {
    
            // Load Native Library (C++); calls JNI_OnLoad()
            System.loadLibrary("Native_Library_File_Name");
        }       
    
        /**
         * A static native method you plan to call.
         */
        public static native void staticNativeMethod(BeanObject bean);
    
        /**
         * A non-static native method you plan to call, to show this also works with 
         * Java class instances.
         */
        public native void instanceNativeMethod(BeanObject bean);
    }
    

    这是在NativeClazz 上使用javah 生成的C++ 头文件“org_stackoverflow_jni_NativeClazz.h”:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class org_stackoverflow_jni_NativeClazz */
    
    #ifndef _Included_org_stackoverflow_jni_NativeClazz
    #define _Included_org_stackoverflow_jni_NativeClazz
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    /*
     * Class:     org_stackoverflow_jni_NativeClazz_staticNativeMethod
     * Method:    staticNativeMethod
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_org_stackoverflow_jni_NativeClazz_staticNativeMethod
      (JNIEnv *, jclass, jobject);
    
    /*
     * Class:     org_stackoverflow_jni_NativeClazz_instanceNativeMethod
     * Method:    instanceNativeMethod
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_org_stackoverflow_jni_NativeClazz_instanceNativeMethod
      (JNIEnv *, jobject, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    这里是实现头文件的 C++ .cpp 文件:

    #include "org_stackoverflow_jni_NativeClazz.h"
    
    using namespace std;
    
    /**************************************************************
     * Static Global Variables to cache Java Class and Method IDs
     **************************************************************/
    static jclass JC_BeanObject;              // declare for each class
    static jmethodID JMID_BeanObject_getFoo;  // declare for each class method
    
    /**************************************************************
     * Declare JNI_VERSION for use in JNI_Onload/JNI_OnUnLoad
     * Change value if a Java upgrade requires it (prior: JNI_VERSION_1_6)
     **************************************************************/
    static jint JNI_VERSION = JNI_VERSION_1_8;
    
    /**************************************************************
     * Initialize the static Class and Method Id variables
     **************************************************************/
    jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    
        // Obtain the JNIEnv from the VM and confirm JNI_VERSION
        JNIEnv* env;
        if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) {
    
            return JNI_ERR;
        }
    
        // Temporary local reference holder
        jclass tempLocalClassRef;
    
        // STEP 1/3 : Load the class id
        tempLocalClassRef = env->FindClass("org/stackoverflow/data/BeanObject");
    
        // STEP 2/3 : Assign the ClassId as a Global Reference
        JC_BeanObject = (jclass) env->NewGlobalRef(tempLocalClassRef);
    
        // STEP 3/3 : Delete the no longer needed local reference
        env->DeleteLocalRef(tempLocalClassRef);
        
        // Load the method id
        JMID_BeanObject_getFoo = env->GetMethodID(JC_BeanObject, "getFoo", "(Ljava/lang/String;)V");
    
        // ... repeat prior line for any other methods of BeanObject
    
        // ... repeat STEPS 1-3 for any other classes; re-use tempLocalClassRef.
    
        // Return the JNI Version as required by method
        return JNI_VERSION;
    }
    
    /**************************************************************
     * Destroy the global static Class Id variables
     **************************************************************/
    void JNI_OnUnload(JavaVM *vm, void *reserved) {
    
        // Obtain the JNIEnv from the VM
        // NOTE: some re-do the JNI Version check here, but I find that redundant
        JNIEnv* env;
        vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION);
    
        // Destroy the global references
        env->DeleteGlobalRef(JC_BeanObject);
        
        // ... repeat for any other global references
    }
    
    /**************************************************************
     * A Static Native Method
     **************************************************************/
    JNIEXPORT void JNICALL Java_org_stackoverflow_jni_NativeClazz_staticNativeMethod
                   (JNIEnv * env, jclass clazz, jobject jBeanObject) {
        
        // Retrieve jstring from the Java Object
        jstring jFoo = (jstring)env->CallObjectMethod(jBeanObject, JMID_BeanObject_getFoo);
    
        // Make accessible to C++
        const char * cFoo = env->GetStringUTFChars(jFoo, NULL);             
    
        // Do something with cFoo...
    
        // Release Resources
        env->ReleaseStringUTFChars(jFoo, cFoo);
        env->DeleteLocalRef(jFoo);
    }
    
    /**************************************************************
     * Instance / Non-Static Native Method
     **************************************************************/
    JNIEXPORT void JNICALL Java_org_stackoverflow_jni_NativeClazz_instanceNativeMethod
                   (JNIEnv * env, jobject selfReference, jobject jBeanObject) {
    
        // Retrieve jstring from the Java Object
        jstring jFoo = (jstring)env->CallObjectMethod(jBeanObject, JMID_BeanObject_getFoo);               
    
        // Make accessible to C++
        const char * cFoo = env->GetStringUTFChars(jFoo, NULL);             
    
        // Do something with cFoo...
    
        // Release Resources
        env->ReleaseStringUTFChars(jFoo, cFoo);
        env->DeleteLocalRef(jFoo);
    }
    

    【讨论】:

    • 关于缓存 jclass/jmethodId/jfieldId 的最佳答案之一!还有一个问题困扰着我。 jmethodID/jfieldID 呢?它们是不透明的结构,我们如何释放/删除它们,或者我们应该如何?或者只是将所有内容(所有已删除的全局 refs、jmethodIDs、jfieIDs)指向 NULL/nullptr 就足够了?
    • @KonstantinBerkow - 方法 ID 和字段 ID 不由本机代码管理;它们由虚拟机管理并且在关联类被卸载之前一直有效。在虚拟机卸载定义类之前,不能显式删除字段 ID 和方法 ID。让虚拟机处理它们。如果将它们设置为 NULL 让您感觉更好,请在释放关联的类后进行,但根本没有必要这样做。
    • 问题:缓存JC_BeanObject 是安全使用缓存JMID_BeanObject_getFoo 的必要条件吗?
    • 参见基本原理部分,@ceztko。根据我在编写它时所读到的内容,并假设它今天仍然适用,如果类 (JC_) 被类加载器卸载,然后重新加载,它会得到一个新的签名。关联的方法签名 (JMID_) 将引用原始签名并且不再有效。缓存类可以防止卸载。见OP原文:ibm.com/developerworks/java/library/j-jni/index.html#notc
    • @ceztko TL;DR 回答:不是第一次调用,而是对同一个加载的 JNI 类的后续调用,它提供了可能永远不会被使用的 99.999998%-场景安全网。
    【解决方案2】:

    这是我实践 IBM 建议的方式。 考虑这样的演示 java 类:

    public class SimpleClazz {
    
        public int value = 10;
    
        public native int getValue();
    
        static {
            // Load Native Library
            System.loadLibrary("The native library name");
        }
    }
    

    对应的jni头文件是这样的:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class SimpleClazz */
    
    #ifndef _Included_SimpleClazz
    #define _Included_SimpleClazz
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     SimpleClazz
     * Method:    getValue
     * Signature: ()I
     */
    JNIEXPORT jint JNICALL Java_SimpleClazz_getValue
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    根据IBM的建议,我们需要缓存使用的类SimpleClazz和对象成员value的字段id。

    在学习了这个好的article之后,我把SimpleClazz缓存在函数JNI_OnLoad中,在加载原生库的时候调用(比如通过System.loadLibrary)。在JNI_Onload 中,我们确实找到了class 并将这个jclass 存储为一个全局字段。

    此外,在getValue的原生实现中,我们使用静态局部变量来缓存value的字段id。这种设计是为了确保这个归档的 id 可以在更好的范围内,而不是在全局范围内。这种设计的缺点是每次调用这个函数都需要和NULL进行比较。这个设计是从The Java Native Interface: Programmer's Guide and Specification这本书的第4.4.1节学到的。

    最后,我们还需要编写函数JNI_OnUnload,当包含本机库的类加载器被垃圾回收时调用。在这个函数中,我们释放了jclass的全局引用。

    我的cpp实现如下图:

    #include <jni.h>
    #include <SimpleClazz.h>
    
    static jclass simpleCls;
    
    // According to http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/invocation.html#JNI_OnLoad
    // The VM calls JNI_OnLoad when the native library is loaded (for example, through System.loadLibrary).
    jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        JNIEnv* env;
        if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
            return JNI_ERR;
        } else {
            jclass localSimpleCls = (*env)->FindClass("SimpleClazz");
    
            if (localSimpleCls == NULL) {
                return JNI_ERR;
            }
            simpleCls = (jclass) (*env)->NewGlobalRef(env, localSimpleCls);
        } 
        return JNI_VERSION_1_6;
    }
    
    
    
    JNIEXPORT jint JNICALL Java_SimpleClazz_getValue(JNIEnv * env, jobject thiz){
        static jfieldID valueID = NULL;
        if (valueID == NULL) {
            valueID = (*env)->GetFieldID(env, simpleCls, "value", "I");
            if (valueID == NULL){
                return JNI_ERR;         // Exception thrown
            }
        }
        jint value = (*env)->GetIntField(env, thiz, valueID);
        return value;
    }
    
    // According to http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/invocation.html#JNI_OnUnload
    // The VM calls JNI_OnUnload when the class loader containing the native library is garbage collected.
    void JNI_OnUnload(JavaVM *vm, void *reserved) {
        JNIEnv* env;
        if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
            // Something is wrong but nothing we can do about this :(
            return;
        } else {
            if (0 != NULL){
                (*env)->DeleteGlobalRef(env, simpleCls);
            }
        }
    }
    

    【讨论】:

    • 在 JNI_OnLoad 方法中进行缓存是个好主意,尤其是在 Android 中,因为 ClassLoader 仅在调用 JNI_OnLoad 的线程上可用(默认情况下),所以这就是我赞成的原因这个答案!
    • 链接“Java 原生接口:程序员指南和规范。”没有回应
    【解决方案3】:

    你可以有一些这样的实用结构:

    typedef struct MYVARIANT_FID_CACHE {
        int cached;
        jclass clazz;
        jfieldID pAddress;
    } MYVARIANT_FID_CACHE;
    
    MYVARIANT_FID_CACHE VARIANTFc;
    
    void cacheMYVARIANTFields(JNIEnv *env, jobject lpObject)
    {
        if (VARIANTFc.cached) return;
        VARIANTFc.clazz = env->GetObjectClass(lpObject);
        VARIANTFc.pAddress = env->GetFieldID(VARIANTFc.clazz, "pAddress", "I");
        VARIANTFc.cached = 1;
    }
    
    VARIANT *getMYVARIANTFields(JNIEnv *env, jobject lpObject, VARIANT *lpStruct)
    {
        if (!VARIANTFc.cached) cacheVARIANT2Fields(env, lpObject);
    
        lpStruct = (VARIANT*)(env->GetIntField(lpObject, VARIANTFc.pAddress));
    
        return lpStruct;
    }
    

    这取自我的问题:https://stackoverflow.com/questions/10617714/how-to-extend-swt-com-support

    有关一些好的示例,请查看os_structs.c,它与 eclipse SWT 实现捆绑在一起。

    注意:上面的代码只是一个例子,可以适应不同的操作系统。它也只是显示“如何访问 java 字段”;对于方法,您可以采用相同的方法。

    【讨论】:

    • 这段代码不可能工作。如果不使用 GlobalReference,则无法跨不同的 JNI 调用缓存 jclasses 或 jobjects。您可以缓存 methodIDs 和 fieldIDs..
    • @EJP:您能否解释一下为什么“它不能跨不同的 JNI 缓存 jclasses 或 jobjects”?此处提供的代码 sn-p 取自 win32 的 eclipse SWT 实现并进行了修改。
    • @Favonious 对于same reason that GlobalReferences exist。 (1)“本地引用在本地方法调用期间有效”,(2)“JNI 函数返回的所有 Java 对象都是本地引用”,以及 (3)“本地引用仅在它们所在的线程中有效”被创建”。
    • “如何扩展 SWT COM 支持”目标不再存在。
    【解决方案4】:

    参考@JoshDM给出的答案

    C 接口与上面给出的 C++ 接口略有不同。你必须这样写:-

    /**************************************************************
     * Initialize the static Class and Method Id variables
     **************************************************************/
    jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    
        // Obtain the JNIEnv from the VM and confirm JNI_VERSION
        JNIEnv* env;
        if (*(vm)->GetEnv(vm, (void**)&env, JNI_VERSION) != JNI_OK) {
    
            return JNI_ERR;
        }
    
        // Temporary local reference holder
        jclass tempLocalClassRef;
    
        // STEP 1/3 : Load the class id
        tempLocalClassRef = (*env)->FindClass(env, "org/stackoverflow/data/BeanObject");
    
        // STEP 2/3 : Assign the ClassId as a Global Reference
        JC_BeanObject = (jclass) (*env)->NewGlobalRef(env, tempLocalClassRef);
    
        // STEP 3/3 : Delete the no longer needed local reference
        (*env)->DeleteLocalRef(env, tempLocalClassRef);
    
        // Load the method id
        JMID_BeanObject_getFoo = (*env)->GetMethodID(env, JC_BeanObject, "getFoo", "(Ljava/lang/String;)V");
    
        // ... repeat prior line for any other methods of BeanObject
    
        // ... repeat STEPS 1-3 for any other classes; re-use tempLocalClassRef.
    
        // Return the JNI Version as required by method
        return JNI_VERSION;
    }
    

    【讨论】:

      【解决方案5】:

      对于简单的情况,我在 C 代码中使用静态字段,使用从我的 java 类调用的 initId 方法进行初始化:

      package demo;
      public class WithNatives {
      
       static {
         initIDs();
       }
      
       private static native void initIDs();
      
      }
      

      在 C 中:

      static jmethodID methodId;
      
      void JNICALL Java_demo_WithNatives_initIDs(JNIEnv *env, jclass clazz)
      {
         // initialize methodId
      }
      

      【讨论】:

      • 我没有展示的是如何初始化methodId字段,但它是基本的JNI。缓存在类加载时自动完成:调用 initIDs 并初始化 methodID 字段
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-12
      • 2014-05-09
      相关资源
      最近更新 更多