目录
一、数据类型
1、Java数据类型
2、JNI数据类型
3、Java同JNI数据类型的对应关系
二、API定义
1、JNI标准API的发展
2、JNIEnv和JavaVM定义
三、异常处理
四、引用操作
1、引用的定义
2、引用API
五、类和对象操作
六、字段和方法操作
在前面两篇文章《Hotspot JNI调用详解》和《Hotspot JNI库文件加载源码解析》详细探讨了JNI调用如何使用,JNI的库文件是如何加载的,下面来详细探讨下JNI API,这API是做什么的,有啥注意事项,这是后续JNI开发的基础。
Java的数据类型分为基本类型(primitive type,又称原生类型或者原始类型)和引用类型(reference type),其中基本类型又分为数值类型,boolean类型和returnAddress类型三类。returnAddress类型在Java语言中没有对应的数据类型,由JVM使用表示指向某个字节码的指针。JVM定义了boolean类型,但是对boolean类型的支持非常有限,boolean类型没有任何专供boolean值使用的字节码指令,java语言表达式操作boolean值,都是使用int类型对应的字节码指令完成的,boolean数组的访问修改共用byte数组的baload和bstore指令;JVM规范中明确了1表示true,0表示false,但是未明确boolean类型的长度,Hotspot使用C++中无符号的char类型表示boolean类型,即boolean类型占8位。数值类型分为整数类型和浮点数类型,如下:
整数类型包含:
浮点类型包括:
float类型和double类型的长度在JVM规范中未明确说明,在Hotspot中float和double就是C++中对应的float和double类型,所以长度分别是32位和64位,所谓双精度是相对单精度而言的,即用于存储小数部分的位数更多,可参考《C++ 名称空间,wchar_t宽字符,浮点数精度,new/delete操作符》。
引用类型在JVM中有三种,类类型(class type),数组类型(array type)和接口类型(interface type),数组类型最外面的一维元素的类型称为该数组的组件类型,组件类型也可以是数组类型,如果组件类型不是元素类型则称为该数组的元素类型,引用类型其实就是C++中的指针。
JVM规范中并没有强引用,软引用,弱引用和虚引用的概念,JVM定义的引用就是强引用,软引用,弱引用和虚引用是JDK结合垃圾回收机制提供的功能支持而已。
参考:Java 的强引用、弱引用、软引用、虚引用
JNI数据类型其实就是Java数据类型在Hotspot中的具体表示或者对应的C/C++类型,类型的定义参考OpenJDK hotspot/src/share/prims/jni.h中,如下图:
部分类型跟CPU架构相关的,通过宏定义jni_md.h引入,如下图:
通常的服务器都是x86_64架构的,其定义的类型如下:
从上面的定义可以得出,JVM中除基本数据类型外,所有的引用类型都是指针,JVM这里只是定义了空白的类来区分不同的引用类型,具体处理指针时会将指针强转成合适的数据类型,如jobject指针会强转成Oop指针,详情可以参考JNIEnv API的实现。
怎么去验证Java数据类型和JNI数据类型的对应关系了?可以通过javah生成的本地方法头文件,比对Java方法和对应的本地方法的参数可以看出两者的对应关系,如下示例:
package jni;
import java.util.List;
public class JniTest{
static
{
System.loadLibrary("HelloWorld");
}
public native static void say(boolean a, byte b, char c, short d, int e, long f, float g, double h, String s, List l,Throwable t,Class cl,
boolean[] a2, byte[] b2, char[] c2, short[] d2, int[] e2, long[] f2, float[] g2, double[] h2,String[] s2);
}
生成的jni_JniTest.h头文件如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class jni_JniTest */
#ifndef _Included_jni_JniTest
#define _Included_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jni_JniTest
* Method: say
* Signature: (ZBCSIJFDLjava/lang/String;Ljava/util/List;Ljava/lang/Throwable;Ljava/lang/Class;[Z[B[C[S[I[J[F[D[Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_jni_JniTest_say
(JNIEnv *, jclass, jboolean, jbyte, jchar, jshort, jint, jlong, jfloat, jdouble, jstring, jobject, jthrowable, jclass, jbooleanArray, jbyteArray, jcharArray, jshortArray, jintArray, jlongArray, jfloatArray, jdoubleArray, jobjectArray);
#ifdef __cplusplus
}
#endif
#endif
本地方法的第一个入参都是JNIEnv指针,第二个入参根据本地方法是否是静态方法区别处理,如果是静态方法,第二个入参是该类的Class即jclass,如果是普通方式则是当前类实例的引用即jobject,其他的入参跟Java方法的入参一样,将Java的数据类型映射到JNI数据类型即可。在Java代码调用本地方法同调用Java一样都是值传递,基本类型参数传递的是参数值,对象类型参数传递的是对象引用,JVM必须跟踪所有传递到本地方法的对象引用,确保所引用的对象没有被垃圾回收掉,本地代码也需要及时通知JVM回收某个对象,垃圾回收器需要能够回收本地代码不再引用的对象。
两者的对应关系具体如下表:
Java数据类型 | JNI数据类型 | x86 C++类型 | 长度 | 备注 |
boolean | jboolean | unsigned char | 8 | |
byte | jbyte | signed char | 8 | |
char | jchar | unsigned short | 16 | |
short | jshort | short | 16 | |
int | jint | int | 16 | |
long | jlong | long | 64 | |
float | jfloat | float | 32 | |
double | jdouble | double | 64 | |
String | jstring | _jstring * | 32/64 | 类指针,在64位机器上默认开启指针压缩,指针长度是32位,否则是64位,不过被压缩的指针仅限于指向堆对象的指针 |
Class | jclass | _jclass * | ||
Throwable | jthrowable | _jthrowable * | ||
boolean[] | jbooleanArray | _jbooleanArray * | ||
byte[] | jbyteArray | _jbyteArray * | ||
char[] | jcharArray | _jcharArray * | ||
short[] | jshortArray | _jshortArray * | ||
int[] | jintArray | _jintArray * | ||
long[] | jlongArray | _jlongArray * | ||
float[] | jfloatArray | _jfloatArray * | ||
double[] | jdoubleArray | _jdoubleArray * | ||
Object[] | jobjectArray | _jobjectArray * |
早期不同厂商的JVM实现提供的JNI API接口有比较大的差异,这些差异导致开发者必须编写不同的代码适配不同的平台,简单的介绍下这些API接口:
1)、JDK 1.0 Native Method Interface
该版本的API因为依赖保守的垃圾回收器且通过C结构体的方式访问Java对象的字段而被废弃
2)、Netscape's Java Runtime Interface
Netscape就是大名鼎鼎的创造了JavaScript语言的网景公司,他制定了一套通用的API(简称JRI),并在设计之初就考虑了可移植性,但是存在诸多的争议。
3)、Microsoft's Raw Native Interface and Java/COM interface
微软的JVM实现提供两种本地接口,Raw Native Interface (RNI)和Java/COM interface,前者很大程度了保持了对JDK JNI API接口的向后兼容,与之最大的区别是本地代码必须通过RNI接口同垃圾回收器交互;后者是一个语言独立的本地接口,Java代码可以同COM对象交互,一个Java类可以对外暴露成一个COM类。
这些API经过各厂商充分讨论,因为各种问题最终没有成为标准API,详情参考Java Native Interface Specification Contents Chapter 1: Introduction
本地代码调用JNI API的入口只有两个JNIEnv和JavaVM类,这两个都在jni.h中定义,如下:
部署后端应用程序的服务器都具备C++运行时,所以只关注C++下的代码即可,即#ifdef __cplusplus下的代码。
JNIENV_和JavaVM_结构体的定义如下:
两者的实现其实是对结构体JNINativeInterface_和JNIInvokeInterface_的简单包装而已,两者定义如下:
从定义上可以看出两者的结构类似于C++中的虚函数表,结构体中没有定义方法而是方法指针,这样一方面实现了C++下对C的兼容,C的结构体中不能定义方法但是可以定义方法指针,C++的结构体基本被扩展成class,可以定义方法和继承;另一方面这种做法实现了接口与实现分离的效果,调用API的本地代码与JVM中具体的实现类解耦,虚拟机可以基于此结构轻松的提供两种实现版本的JNI API,比如其中一个对入参严格校验,另一个只做关键参数最少的校验,让方法指针指向不同的实现即可,API调用方完全无感知。官方文档中提供了一张图描述这种结构,如下图:
其中的JNI interface pointer就是传入本地方法的参数JNIEnv指针, 该指针指向的JNIEnv对象本身包含了一个指向JNINativeInterface_结构体的指针,即图中的Pointer,JNINativeInterface_结构体在内存中相当于一个指针数组,即图中的Array of pointers to JNI functions,指针数组中的每个指针都是具体的方法实现的指针。注意JVM规范要求同一个线程内多次JNI调用接收的JNIEnv或者JavaVM指针都是同一个指针,且该指针只在该线程内有效,因此本地代码不能讲该指针从当前线程传递到另一个线程中。
JNINativeInterface_和JNIInvokeInterface_两者的赋值如下:
前面的三个NULL都是为未来兼容COM对象保留的,JNINativeInterface_中第四个NULL是为未来的一个类相关的JNI操作保留的。结构体JNINativeInterface_和JNIInvokeInterface_包含的方法实现在跟jni.h同目录的jni.cpp中,JNIEnv和JavaVM类的初始化可以参考《Hotspot启动和初始化源码解析》。
所有的JNI方法同大多数的C库函数一样不检查传入的参数的正确性,这点由调用方负责检查,如果参数错误可能导致JVM直接宕机。大多数情况下,JNI方法通过返回一个特定的错误码或者抛出一个Java异常的方式报错,调用方可以通过ExceptionOccurred()方法判断是否发生了异常,如本地方法调用Java方法,判断Java方法执行期间是否发生了异常,并通过该方法获取异常的详细信息。
JNI允许本地方法抛出或者捕获Java异常,未被本地方法捕获的异常会向上传递给方法的调用方。本地方法有两种方式处理异常,一种是直接返回,导致异常在调用本地方法的Java方法中被抛出;一种是调用ExceptionClear()方法清除这个异常,然后继续执行本地方法的逻辑。当异常产生,本地方法必须先清除该异常才能调用其他的JNI方法,当异常尚未处理时,只有下列方法可以被安全调用:
异常处理相关API如下:
测试用例如下:
package jni;
public class ThrowTest {
static {
System.load("/home/openjdk/cppTest/ThrowTest.so");
}
public static native void rethrowException(Exception e);
public static native void handlerException();
public static void main(String[] args) {
handlerException();
rethrowException(new UnsupportedOperationException("Unsurpported ThrowTest"));
}
}
#include "ThrowTest.h"
#include
JNIEXPORT void JNICALL Java_jni_ThrowTest_rethrowException(JNIEnv * env,
jclass cls, jthrowable e) {
printf("Java_jni_ThrowTest_rethrowException\n");
env->Throw(e);
}
void throwNewException(JNIEnv * env) {
printf("throwNewException\n");
jclass unsupportedExceptionCls = env->FindClass(
"java/lang/UnsupportedOperationException");
env->ThrowNew(unsupportedExceptionCls, "throwNewException Test\n");
}
JNIEXPORT void JNICALL Java_jni_ThrowTest_handlerException(JNIEnv * env,
jclass cls) {
throwNewException(env);
jboolean result = env->ExceptionCheck();
printf("ExceptionCheck result->%d\n", result);
env->ExceptionDescribe();
result = env->ExceptionCheck();
printf("ExceptionCheck for ExceptionDescribe result->%d\n", result);
throwNewException(env);
jthrowable e = env->ExceptionOccurred();
if (e) {
printf("ExceptionOccurred not null\n");
} else {
printf("ExceptionOccurred null\n");
}
env->ExceptionClear();
printf("ExceptionClear\n");
e = env->ExceptionOccurred();
if (e) {
printf("ExceptionOccurred not null\n");
} else {
printf("ExceptionOccurred null\n");
}
}
jni.h中关于引用只定义了一个枚举,如下:
jobjectRefType表示引用类型,仅限于本地方法使用,具体如下:
综上,这里的引用其实就是Java中new一个对象返回的引用,本地引用相当于Java方法中的局部变量对Java对象实例的引用,全局引用相当于Java类的静态变量对Java对象实例的引用,其本质跟C++智能指针模板一样,是对象指针的二次包装,通过包装避免了该指针指向的对象被垃圾回收器回收掉,因此JNI中通过隐晦的引用访问Java对象的消耗比通过指针直接访问要高点,但是这是JVM对象和内存管理所必须的。
相关API如下:
。
PushLocalFrame和PopLocalFrame两个都是配合使用,常见于方法执行过程中产生的本地引用需要尽快释放掉,如下图:
WITH_LOCAL_REFS和END_WITH_LOCAL_REFS两个宏的定义如下:
NewGlobalRef的使用场景通常是初始化C/C++的全局属性,需要通过全局引用确保该属性指向的某个Java对象实例不被垃圾回收器回收掉,如下图:
NewLocalRef的使用场景不多,通常是用来检测目标对象是否已经被回收掉了,如果被回收了则该方法返回NULL,如下图:
创建了一个新对象并返回该对象的本地引用通常直接调用JNIHandles::make_local实现,jni_NewLocalRef的实现也是通过该方法完成,所以NewLocalRef被直接使用的不多,如下:
类操作API如下:
对象操作相关API:
测试用例如下:
package jni;
class A{
}
public class ObjTest extends A {
static {
System.load("/home/openjdk/cppTest/ObjTest.so");
}
public ObjTest() {
System.out.println("default");
}
public ObjTest(int age) {
System.out.println("param Construtor,age->"+age);
}
public native static void test(Object a);
public static void main(String[] args) {
test(new ObjTest());
}
}
#include "ObjTest.h"
#include
JNIEXPORT void JNICALL Java_jni_ObjTest_test
(JNIEnv * env, jclass jcl,jobject obj){
jcl=env->GetObjectClass(obj);
jclass objACls=env->FindClass("jni/A");
jboolean result=env->IsAssignableFrom(jcl,objACls);
printf("IsAssignableFrom result->%d\n",result);
jobject objTest=env->AllocObject(jcl);
printf("AllocObject succ \n");
jmethodID defaultConst=env->GetMethodID(jcl,"","()V");
objTest=env->NewObject(jcl,defaultConst);
printf("default construct new succ \n");
jmethodID paramConst=env->GetMethodID(jcl,"","(I)V");
objTest=env->NewObject(jcl,paramConst,12);
printf("param construct succ new \n");
jclass superCls=env->GetSuperclass(jcl);
jobject superObj=env->AllocObject(superCls);
result=env->IsInstanceOf(superObj,objACls);
printf("IsInstanceOf result->%d\n",result);
jobject objTest2=env->NewLocalRef(objTest);
result=env->IsSameObject(objTest2,objTest);
printf("IsSameObject result->%d\n",result);
}
1、jfieldID和jmethodID定义
JNI中使用jfieldID来标识一个某个类的字段,jmethodID来标识一个某个类的方法,jfieldID和jmethodID都是根据他们的字段名(方法名)和描述符确定的,通过jfieldID读写字段或者通过jmethodID调用方法都会避免二次查找,但是这两个ID不能阻止该字段或者方法所属的类被卸载,如果类被卸载了则jfieldID和jmethodID失效了需要重新计算,因此如果希望长期使用jfieldID和jmethodID则需要保持对该类的持续引用,即建立对该类的全局引用,两者的定义在jni.h中,如下:
这两个并非常规的字符串或者数字形式的ID,而是一个指针,指向实际保存字段信息和方法的该类的Klass。
2、API说明
相关的API如下:
void
(V
)。注意上述方法和字段操作都是针对非静态的方法和字段的,对静态的方法和字段操作的API如下:
注意在调用方法或者设置属性传参数时,需要密切关注参数类型,尤其是基本类型,只有字段或者方法声明明确使用了基本类型传参才能使用基本类型,否则必须通过Integer等包装类的构造方法将基本类型转换为对应包装类的对象;另一个需要注意的就是可变参数类型,Java中可以传入数量可变的参数,这些参数最终会被编译器转换成一个数组,即这类参数类型实际是一个数组,因此传参时不能跟Java一样,而需要显示的传入一个数组类型。示例如下:
package jni;
import java.util.Arrays;
import java.util.List;
class SuperA{
public void say(){
System.out.println("say SuperA");
}
public void add(int a,int b){
System.out.println("SuperA add a->"+a+",b->"+b+",result->"+(a+b));
}
}
public class FiledMethodTest extends SuperA {
static {
System.load("/home/openjdk/cppTest/FiledMethodTest.so");
}
private List list;
private boolean boolField;
private byte byteField;
private char charField;
private short shortField;
private int intField;
private long longField;
private float floatField;
private double doubleField;
private static int staticFiled;
public FiledMethodTest() {
list= Arrays.asList(1,2);
boolField=false;
byteField=11;
charField='c';
shortField=12;
intField=13;
longField=14;
floatField=15.0f;
doubleField=16.0;
staticFiled=17;
}
@Override
public void say() {
System.out.println("say FiledMethodTest");
}
public List getList() {
return list;
}
private static void printList(List list){
if(list==null){
System.out.println("list is null");
}else {
System.out.println("list->" + list);
}
}
private void printObj() {
System.out.println("FiledMethodTest{" +
"list=" + list +
", boolField=" + boolField +
", byteField=" + byteField +
", charField=" + charField +
", shortField=" + shortField +
", intField=" + intField +
", longField=" + longField +
", floatField=" + floatField +
", doubleField=" + doubleField +
", staticFiled=" + staticFiled +
'}');
}
public native static void test(FiledMethodTest a);
public static void main(String[] args) throws Exception {
FiledMethodTest a=new FiledMethodTest();
System.out.println("start test");
test(a);
}
}
#include "FiledMethodTest.h"
#include
JNIEXPORT void JNICALL Java_jni_FiledMethodTest_test(JNIEnv * env, jclass jcl,
jobject obj) {
//注意字段和方法描述符中如果是其他的类,必须带上后面的分号
jfieldID listId = env->GetFieldID(jcl, "list", "Ljava/util/List;");
jfieldID boolFieldId = env->GetFieldID(jcl, "boolField", "Z");
jfieldID byteFieldId = env->GetFieldID(jcl, "byteField", "B");
jfieldID charFieldId = env->GetFieldID(jcl, "charField", "C");
jfieldID shortFieldId = env->GetFieldID(jcl, "shortField", "S");
jfieldID intFieldId = env->GetFieldID(jcl, "intField", "I");
jfieldID longFieldId = env->GetFieldID(jcl, "longField", "J");
jfieldID floatFieldId = env->GetFieldID(jcl, "floatField", "F");
jfieldID doubleFieldId = env->GetFieldID(jcl, "doubleField", "D");
jfieldID staticFiledId = env->GetStaticFieldID(jcl, "staticFiled", "I");
jmethodID printListId = env->GetStaticMethodID(jcl, "printList",
"(Ljava/util/List;)V");
jmethodID printObjId = env->GetMethodID(jcl, "printObj", "()V");
jmethodID getListId = env->GetMethodID(jcl, "getList",
"()Ljava/util/List;");
jmethodID sayId=env->GetMethodID(jcl,"say","()V");
jmethodID addId=env->GetMethodID(jcl,"add","(II)V");
jclass arrayListCls = env->FindClass("java/util/ArrayList");
jmethodID list_costruct = env->GetMethodID(arrayListCls, "", "()V");
jmethodID listAddId = env->GetMethodID(arrayListCls, "add",
"(Ljava/lang/Object;)Z");
jclass arraysCls=env->FindClass("java/util/Arrays");
jmethodID asListId=env->GetStaticMethodID(arraysCls,"asList","([Ljava/lang/Object;)Ljava/util/List;");
jclass intergerCls = env->FindClass("java/lang/Integer");
jmethodID interger_costruct = env->GetMethodID(intergerCls, "",
"(I)V");
//如果找不到方法或者字段不会直接报错,需要手动执行异常检查
if (env->ExceptionCheck()) {
jthrowable err = env->ExceptionOccurred();
env->Throw(err);
}
jobject listObj = env->GetObjectField(obj, listId);
env->CallStaticVoidMethod(jcl, printListId, listObj);
jobject listObj2 = env->CallObjectMethod(obj, getListId);
jboolean issame = env->IsSameObject(listObj, listObj2);
printf("issame->%d\n", issame);
jboolean boolField = env->GetBooleanField(obj, boolFieldId);
printf("boolField->%d\n", boolField);
jbyte byteField = env->GetByteField(obj, byteFieldId);
printf("byteField->%d\n", byteField);
jchar charField = env->GetCharField(obj, charFieldId);
printf("charField->%d\n", charField);
jshort shortField = env->GetShortField(obj, shortFieldId);
printf("shortField->%d\n", shortField);
jint intField = env->GetIntField(obj, intFieldId);
printf("intField->%d\n", intField);
jlong longField = env->GetLongField(obj, longFieldId);
printf("longField->%d\n", longField);
jfloat floatField = env->GetFloatField(obj, floatFieldId);
printf("floatField->%f\n", floatField);
jdouble doubleField = env->GetDoubleField(obj, doubleFieldId);
printf("doubleField->%f\n", doubleField);
jint staticFiled = env->GetStaticIntField(jcl, staticFiledId);
printf("staticFiled->%d\n", staticFiled);
//JNI中没有对基本类型的自动装箱拆箱机制,必要时需要手动包装
jobject intObj = env->NewObject(intergerCls, interger_costruct, 3);
jobject intObj2 = env->NewObject(intergerCls, interger_costruct, 4);
// jobject newList = env->NewObject(arrayListCls, list_costruct);
//add方法接受的参数实际是一个对象,因此需要手动包装
// env->CallBooleanMethod(newList, listAddId, intObj);
// env->CallBooleanMethod(newList, listAddId, intObj2);
//Arrays.asList方法在Java中是不可变参数,实际多个参数最终会被转变成数组,因此这里的入参必须是数组
jobjectArray objArray=env->NewObjectArray(2,intergerCls,intObj);
env->SetObjectArrayElement(objArray,1,intObj2);
jobject newList=env->CallStaticObjectMethod(arraysCls,asListId,objArray);
env->SetObjectField(obj, listId, newList);
env->SetBooleanField(obj, boolFieldId, 1);
env->SetByteField(obj, byteFieldId, 21);
env->SetCharField(obj, charFieldId, 'd');
env->SetShortField(obj, shortFieldId, 22);
env->SetIntField(obj, intFieldId, 23);
env->SetLongField(obj, longFieldId, 24);
env->SetFloatField(obj, floatFieldId, 25.0);
env->SetDoubleField(obj, doubleFieldId, 26.0);
env->SetStaticIntField(jcl, staticFiledId, 27);
env->CallVoidMethod(obj, printObjId);
jclass superCls=env->GetSuperclass(jcl);
jmethodID superSayId=env->GetMethodID(superCls,"say","()V");
//如果子类没有覆写则使用父类的实现,否则使用子类覆写的实现
env->CallVoidMethod(obj,sayId);
env->CallVoidMethod(obj,addId,3,4);
//使用jclass的方法实现,可以是子类的也可以是父类的,取决于后面的methodId
env->CallNonvirtualVoidMethod(obj,jcl,sayId);
env->CallNonvirtualVoidMethod(obj,jcl,superSayId);
env->CallNonvirtualVoidMethod(obj,superCls,sayId);
env->CallNonvirtualVoidMethod(obj,superCls,superSayId);
}
注意上述示例中printObj和printList方法都是私有方法,但是通过JNI接口一样可以正常调用,说明JNI无视Java的访问权限控制,可以访问任何方法和字段。