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中不同类型的属性有不同的方法获取属性值,如下所示:

获取属性值得函数名

返回值类型

GetObjectField

jobject

GetBooleanField

jboolean

GetByteField

jbyte

GetCharField

jchar

GetShortField

jshort

GetIntField

jint

GetLongField

jlong

GetFloatField

jfloat

GetDoubleField

jdouble

对于调用实例成员方法,和上面访问属性的过程类似,首先需要后去这个方法的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);

其中<type>表示对应的类型,如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/

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏项勇

笔记45 | 代码性能优化建议[转]

15060
来自专栏大史住在大前端

野生前端的数据结构基础练习(2)——队列

循环队列书中并没有提及,它是一种特殊的队列。简单理解就是将基本队列只当做存储结构,而使用front和rear两个指针分别代表队列的头和尾,实际对外表现的队列是f...

22930
来自专栏锦小年的博客

python学习笔记6.5-类中描述符的使用

描述符(Descriptor)就是以特殊方法get(), set(), delete()的形式实现了三个核心的属性访问操作(set,get,delete)的类。...

22090
来自专栏恰童鞋骚年

《你必须知道的.NET》读书笔记一:小OO有大智慧

此篇已收录至《你必须知道的.Net》读书笔记目录贴,点击访问该目录可以获取更多内容。

6620
来自专栏racaljk

C++11 特性:成员函数引用限定 (Reference qualifier)

学了这么多年C++今天拜读scott meyes的more effective cpp第一次看到这种写法...

13650
来自专栏Crossin的编程教室

【编程课堂】有序字典 OrderedDict

编程课堂将和每周一坑一样,成为本教室公众号的一个长期固定栏目。每期讲解一个编程知识点,包括但不限于 Python 语法、模块介绍、编程小技巧等。用简短的篇幅,让...

40980
来自专栏为数不多的Android技巧

ART深度探索开篇:从Method Hook谈起

Android上的热修复框架 AndFix 想必已经是耳熟能详,它的原理实际上很简单:方法替换——Java层的每一个方法在虚拟机实现里面都对应着一个ArtMet...

29910
来自专栏程序员的酒和故事

跟Google学写代码--Chromium工程中用到的C++11特性

Ttile 跟Google学写代码--Chromium工程中用到的C++11特性 Chromium是一个伟大的、庞大的开源工程,很多值得我们学习的地方。 《跟...

46940
来自专栏CSDN技术头条

【问底】静行:FastJSON实现详解

还记得电影《功夫》中火云邪神的一句话:天下功夫,无坚不破,唯快不破。在程序员的世界中,“快”一直是大家苦苦修炼,竞相追逐的终极目标之一,甚至到了“不择手段”、“...

33970
来自专栏chafezhou

小说python2和python3的差异

21740

扫码关注云+社区

领取腾讯云代金券