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