前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JNI基础知识学习汇总

JNI基础知识学习汇总

原创
作者头像
chain
发布2018-06-05 13:09:36
1.4K12
发布2018-06-05 13:09:36
举报
文章被收录于专栏:开发 & 算法杂谈

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),示例代码如下所示:

代码语言:txt
复制
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++头文件:

执行命令行:

代码语言:txt
复制
javac HelloJNI.java

javah -jni -cp . HelloJNI

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

代码语言:txt
复制
/* 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)实现文件,示例代码如下所示:

代码语言:txt
复制
#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),执行如下命令:

代码语言:txt
复制
 gcc -Wall -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\linux" -shared -o libhello.so HelloJNI.c

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

代码语言:txt
复制
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结构如下所示:

代码语言:txt
复制
/*
 * 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方法的指针。该指针只在创建它的线程中有效,不能够跨线程传递,其声明如下:

代码语言:txt
复制
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语言来说,其结构如下所示:

代码语言:txt
复制
/*
 * 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++来说,其结构如下所示:

代码语言:txt
复制
/*
 * 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来新建和删除全局引用:

代码语言:txt
复制
jobject NewGlobalRef(JNIEnv *env, jobject obj);

void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

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

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

代码语言:txt
复制
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来创建和删除弱全局引用:

代码语言:txt
复制
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);

void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

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

代码语言:txt
复制
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对象,然后返回给调用函数,示例代码如下所示:

代码语言:txt
复制
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中可以通过类名查找一个类,方法如下所示:

代码语言:txt
复制
jclass FindClass(JNIEnv *env, const char *name);

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

代码语言:txt
复制
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 函数推出后就会失效,因此上述方法第二次调用会出现不正常结果,解决方法就是把局部引用转换成全局引用,修改后的示例代码如下所示:

代码语言:txt
复制
   // 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中很类似,指定类信息,并且选择合适的构造器传入参数,主要有三种创建对象的方式:

代码语言:txt
复制
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);

从对象获取类信息:

代码语言:txt
复制
jclass GetObjectClass(JNIEnv *env, jobject obj);

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

代码语言:txt
复制
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如下所示:

代码语言:txt
复制
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

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

代码语言:txt
复制
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类的静态属性,可以通过类似上述的方法获取:

代码语言:txt
复制
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);

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

代码语言:txt
复制
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字符串,可以采用如下方法:

代码语言:txt
复制
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);

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

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

代码语言:txt
复制
jsize GetStringLength(JNIEnv *env, jstring string);

jsize GetStringUTFLength(JNIEnv *env, jstring string);

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

代码语言:txt
复制
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);

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

代码语言:txt
复制
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);

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

代码语言:txt
复制
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

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

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

代码语言:txt
复制
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/

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JNI介绍
  • JNI编程示例
  • JNI核心数据结构
  • JNI中的引用类型
  • JNI中类操作
  • JNI中对象操作
  • JNI中字符串与数组操作
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档