JNI基础知识学习汇总

JNI介绍

JNI(Java Native Interface),也就是java本地接口,主要是用来支持和本地代码之间的互动-在Java程序中调用native code或者在native code中潜入Java虚拟机调用Java代码。

JNI编程优缺点可以归结如下:

优点
* native code的平台相关性可以在相应的平台编程中体现自己的优势
* native code代码重用
* native code直接操作底层更加高效

缺点
* 从JVM环境且话到native code上下文环境比较耗时
* JNI编程如果操作不当,容易引起JVM的崩溃
* JNI编程如果操作不当,容易引起内存泄漏

JNI编程示例

1、编写Java类(HelloJNI),示例代码如下所示:

public class HelloJNI {
   static {
      // 加载共享库(windows中为hello.dll,Unix中为libhello.so)
      System.loadLibrary("hello"); // hello.dll (Windows) or libhello.so
   }
   // 声明native方法
   void private native void sayHello();

   // 调用native方法
   public static void main(String[] args) {
      new HelloJNI().sayHello();  // invoke the native method
   }
}

2、编译HelloJNI并创建对应的C/C++头文件:

执行命令行:

javac HelloJNI.java

javah -jni -cp . HelloJNI

其中javah利用生成的.class文件创建一个包含native方法的头文件,内容如下所示:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

3、编写C(HelloJNI.c)实现文件,示例代码如下所示:

#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"

// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
   printf("Hello World!\n");
   return;
}

4、GCC编译生成共享库(libhello.so),执行如下命令:

 gcc -Wall -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\linux" -shared -o libhello.so HelloJNI.c

5、Java程序调用native方法,执行如下命令:

java -cp . -Djava.library.path=. HelloJNI

上述示例简单介绍了JNI编程的一般步骤,下面将详细介绍JNI编程相关的一些知识。

JNI核心数据结构

JNI定义了两个核心的数据结构,JavaVM以及JNIEnv。JavaVM 是 Java虚拟机在 JNI 层的代表,JNI 全局只有一个;而JNIEnv是 JavaVM 在线程中的代表,每个线程都有一个,JNI 中可能有很多个 JNIEnv。

JNIEnv主要的作用就是有如下:
* 调用 Java 函数:JNIEnv 代表 Java 运行环境,可以使用 JNIEnv 调用 Java 中的代码
* 操作 Java 对象:Java 对象传入 JNI 层就是 Jobject 对象,需要使用 JNIEnv 来操作这个 Java 对象

JNIEnv从JavaVM中可以获得,JavaVM结构如下所示:

/*
 * JNI invocation interface.
 */
struct JNIInvokeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;

    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

/*
 * C++ version.
 */
struct _JavaVM {
    const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version)
    { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

对于C语言来说:
* 创建JNIEnv:JNIInvokeInterface是C语言环境中的JavaVM的结构体,调用(*AttachCurrentThread)(JavaVM*, JNIEnv**, void*)方法就可以获取JNIEnv结构体;
* 释放JNIEnv:调用JavaVM结构体中的(DetachCurrentThread)(JavaVM)可以释放本线程中的JNIEnv

对于C++来说:
* 创建JNIEnv:__JavaVM是C++中的JavaVM结构体,调用jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)就可以获得JIN结构体;
* 释放JNIEnv:调用JavaVM中的jint DetachCurrentThread()的方法,就可以释放本县城中的JNIEnv。

JNI类型是一个指向全部JNI方法的指针。该指针只在创建它的线程中有效,不能够跨线程传递,其声明如下:

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

对于C语言来说,其结构如下所示:

/*
 * Table of interface function pointers.
 */
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;

    jint        (*GetVersion)(JNIEnv *);

    ... ...

    jobject     (*NewDirectByteBuffer)(JNIEnv*, void*, jlong);
    void*       (*GetDirectBufferAddress)(JNIEnv*, jobject);
    jlong       (*GetDirectBufferCapacity)(JNIEnv*, jobject);

    /* added in JNI 1.6 */
    jobjectRefType (*GetObjectRefType)(JNIEnv*, jobject);
};

而对于C++来说,其结构如下所示:

/*
 * C++ object wrapper.
 *
 * This is usually overlaid on a C struct whose first element is a
 * JNINativeInterface*.  We rely somewhat on compiler behavior.
 */
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }

    ... ... 

    jlong GetDirectBufferCapacity(jobject buf)
    { return functions->GetDirectBufferCapacity(this, buf); }

    /* added in JNI 1.6 */
    jobjectRefType GetObjectRefType(jobject obj)
    { return functions->GetObjectRefType(this, obj); }
#endif /*__cplusplus*/
};

JNI中的引用类型

JNI中引用类型分为三种,全局引用,局部引用以及弱全局引用。

全局引用可以跨方法(本地方法返回后仍然有效),跨线程使用,直到手动释放才会失效。该引用不会被GC回收。

可以通过下面的两个api来新建和删除全局引用:

jobject NewGlobalRef(JNIEnv *env, jobject obj);

void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

局部引用是JVM负责的引用类型,其被JVM分配管理,并占用JVM的资源。局部引用在native方法返回后被自动回收。局部引用只在创建它们的线程中有效,不能跨线程传递。

可以通过一下两个api来创建和删除局部引用:

jobject NewLocalRef(JNIEnv *env, jobject ref);

void DeleteLocalRef(JNIEnv *env, jobject localRef);

虚拟机将确保每个本地方法至少可以创建16个局部引用。但是在如今的场景中,16个局部引用已经远远不能满足开发需求了。为了为了解决这个问题,JNI提供了查询可用引用容量的方法jint EnsureLocalCapacity(JNIEnv *env, jint capacity),我们在创建超出限制的引用时最好先确认是否有足够的空间。

弱全局引用是一种特殊的全局引用。跟普通的全局引用不同的是,一个弱全局引用允许Java对象被垃圾回收器回收。当垃圾回收器运行的时候,如果一个对象仅被弱全局引用所引用,则这个引用将会被回收。一个被回收了的弱引用指向NULL,开发者可以将其与NULL比较来判定该对象是否可用。

可以通过以下两个api来创建和删除弱全局引用:

jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);

void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

局部引用使用不当有可能会引用内存泄漏。比如某些情况下,我们可能需要在native method中创建大量的局部引用,会导致native memory的内存泄漏,如果在native method返回之前native memory以及被用光,会导致OOM,如下是一个局部引用引发内存泄漏的一个示例:

Java 代码部分
 class TestLocalReference { 
 private native void nativeMethod(int i); 
 public static void main(String args[]) { 
         TestLocalReference c = new TestLocalReference(); 
         //call the jni native method 
         c.nativeMethod(1000000); 
 }  
 static { 
 //load the jni library 
 System.loadLibrary("StaticMethodCall"); 
 } 
 } 

JNI 代码,nativeMethod(int i) 的 C 语言实现
 #include<stdio.h> 
 #include<jni.h> 
 #include"TestLocalReference.h"
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 
 (JNIEnv * env, jobject obj, jint count) 
 { 
 jint i = 0; 
 jstring str; 

 for(; i<count; i++) 
    str = (*env)->NewStringUTF(env, "0"); 
 } 

上述示例运行结果会导致OOM,主要的原因是创建了越来越多的局部引用,导致JNI内部的 局部引用表 内存溢出。

对上述代码稍作修改,在子函数中创建String对象,然后返回给调用函数,示例代码如下所示:

JNI 代码,nativeMethod(int i) 的 C 语言实现
 #include<stdio.h> 
 #include<jni.h> 
 #include"TestLocalReference.h"
 jstring CreateStringUTF(JNIEnv * env) 
 { 
 return (*env)->NewStringUTF(env, "0"); 
 } 
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 
 (JNIEnv * env, jobject obj, jint count) 
 { 
 jint i = 0; 
 for(; i<count; i++) 
 { 
    str = CreateStringUTF(env); 
 } 
 } 

修改之后的实例运行结果和没有修改之前执行结果一样,同样导致了 局部引用表 内存溢出(尽管有一个函数的退栈过程)。

每当线程从Java环境切换到native code时,JVM都会分配一块内存,创建一个 局部引用表 ,这个表用来存放native method执行中创建的所有 局部引用。因此上述示例调用NewStringUTF在Java堆中创建一个String对象后,在 局部引用表 中就会相应增加一项。

JNI中的局部引用并不是nativde code中的局部变量,两者的区别可以总结如下:
* 局部变量存储在线程堆栈中,而 Local Reference 存储在 Local Ref 表中
* 局部变量在函数退栈后被删除,而 Local Reference 在调用 DeleteLocalRef() 后才会从 Local Ref 表中删除,并且失效,或者在整个 Native Method 执行结束后被删除。
* 以在代码中直接访问局部变量,而 Local Reference 的内容无法在代码中直接访问,必须通过 JNI function 间接访问。JNI function 实现了对 Local Reference 的间接访问,JNI function 的内部实现依赖于具体 JVM。

因此在JNI编程时需要正确控制局部引用的生命周期。

JNI中类操作

JNI中可以通过类名查找一个类,方法如下所示:

jclass FindClass(JNIEnv *env, const char *name);

在native code中由于考虑到会多次调用某个方法,一般情况下会定义成static类型,然后在函数第一次调用的时候去查询对应的类型,后续对函数的调用就不用再查询相同的类型了,示例代码如下:

static jclass classInteger;
static jmethodID midIntegerInit;

jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {

   // Get a class reference for java.lang.Integer if missing
   if (NULL == classInteger) {
      printf("Find java.lang.Integer\n");
      classInteger = (*env)->FindClass(env, "java/lang/Integer");
   }
   if (NULL == classInteger) return NULL;

   // Get the Method ID of the Integer's constructor if missing
   if (NULL == midIntegerInit) {
      printf("Get Method ID for java.lang.Integer's constructor\n");
      midIntegerInit = (*env)->GetMethodID(env, classInteger, "<init>", "(I)V");
   }
   if (NULL == midIntegerInit) return NULL;

   // Call back constructor to allocate a new instance, with an int argument
   jobject newObj = (*env)->NewObject(env, classInteger, midIntegerInit, number);
   printf("In C, constructed java.lang.Integer with number %d\n", number);
   return newObj;
}

然而,FindClass返回的class的局部引用,当native 函数推出后就会失效,因此上述方法第二次调用会出现不正常结果,解决方法就是把局部引用转换成全局引用,修改后的示例代码如下所示:

   // Get a class reference for java.lang.Integer if missing
   if (NULL == classInteger) {
      printf("Find java.lang.Integer\n");
      // FindClass returns a local reference
      jclass classIntegerLocal = (*env)->FindClass(env, "java/lang/Integer");
      // Create a global reference from the local reference
      classInteger = (*env)->NewGlobalRef(env, classIntegerLocal);
      // No longer need the local reference, free it!
      (*env)->DeleteLocalRef(env, classIntegerLocal);
   }

注意jmethodID以及jfieldID不是jobject类型,因此不能够创建全局引用。

JNI中对象操作

创建对象和Java中很类似,指定类信息,并且选择合适的构造器传入参数,主要有三种创建对象的方式:

jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...);

jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);

jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);

从对象获取类信息:

jclass GetObjectClass(JNIEnv *env, jobject obj);

当有一个Java对象,如何才能够操作这个对象中的属性?要操作一个属性,一般要活的该属性在JVM中的唯一标识ID,然后再通过Get和Set方法去操作属性。获取属性ID方法如下所示:

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

当获取到属性ID的时候,就可以后的属性的值了,JNI中不同类型的属性有不同的方法获取属性值,如下所示:

获取属性值得函数名返回值类型
GetObjectFieldjobject
GetBooleanFieldjboolean
GetByteFieldjbyte
GetCharFieldjchar
GetShortFieldjshort
GetIntFieldjint
GetLongFieldjlong
GetFloatFieldjfloat
GetDoubleFieldjdouble

对于调用实例成员方法,和上面访问属性的过程类似,首先需要后去这个方法的ID,然后根据这个ID来进行相应的操作。获取实例方法ID如下所示:

jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

JNI中,根据不同的参数和返回值类型需要调用不同的方法。对于传入的参数类型需要在方法名的后面使用不同的后缀来标识,如下所示:

NativeType Call<type>Method(JNIEnv *env, jobject obj, 
jmethodID methodID, ...);

NativeType Call<type>MethodA(JNIEnv *env, jobject obj, 
jmethodID methodID, const jvalue *args);

NativeType Call<type>MethodV(JNIEnv *env, jobject obj, 
jmethodID methodID, va_list args);

其中表示对应的类型,如Void、Boolean、Object、Long、Byte等。

关于Java类的静态属性,可以通过类似上述的方法获取:

jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);

调用静态方法和上述调用对象的方法类似:

jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);

JNI中字符串与数组操作

JNI中,如果需要使用一个Java字符串,可以采用如下方法:

jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);

jstring NewStringUTF(JNIEnv *env, const char *bytes);

获取Java字符串的长度,可以采用如下方法:

jsize GetStringLength(JNIEnv *env, jstring string);

jsize GetStringUTFLength(JNIEnv *env, jstring string);

JNI中可以使用如下方法创建一个对象数组:

jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);

如果想要后去对象数组中的某个元素,可以使用如下方法:

jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);

当然,对于基本类型的数组,可以使用如下方法:

ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

其中ArrayType就是数组类型,如jbooleanArray,jintArray以及jdoubleArray等

获取基本类型的数组,可以使用如下方法:

NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);

参考

http://jiangwenfeng762.iteye.com/blog/1500131

https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html

http://www.2cto.com/kf/201407/319308.html

https://www.zybuluo.com/cxm-2016/note/566590

http://www.ibm.com/developerworks/cn/java/j-lo-jnileak/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值