JNI 工作流程

  1. java层调用system.load方法。
  2. 通过classloader拿到了so文件的绝对路径,然后调用nativeload()方法。
  3. 通过linux下的dlopen方法,加载并查找so库里的方法。
  4. 当前线程下的 JNIENV 会将所有的jni方法注册到了同一个Jvm中,so和class到了同一个进程空间

    (人脸项目中就是在Strom的一个Worker JVM,多个executor线程共享一个faceEengine对象)(JNIENV 代表了java在本线程的运行环境,每个线程都有一个)。
  5. 通过当前线程的jnienv即可调用对应的对象方法了。

JNI 内存模型

Java应用程序所涉及的内存可以从逻辑上划分为两部分:Heap Memory和Native Memory。

Java应用程序都是在Java Runtime Environment(JRE)中运行,而JRE本身就是由Native语言(如:C/C++)编写的程序。

(JVM只是JRE的一部分,JVM的内存模型属于另一话题)

所以包含关系大致这样:(JRE (JVM (Heap Mem, Native Memory) ) )

  1. Heap Memory:供Java应用程序使用,所有java对象的内存都是从这里分配的,它物理上不连续,但是逻辑上是连续的。可通过java命令行参数“-Xms, -Xmx”大设置Heap初始值和最大值。
  2. Native Memory:Java Runtime进程使用,没有相应的参数来控制其大小,由操作系统分配给Runtime进程的可用内存,大小依赖于操作系统进程的最大值。

Native Memory的主要作用如下:

  • 管理java heap的状态数据(用于GC);
  • JNI调用,也就是Native Stack,JNI内存分配其实与Native Memory有很大关系
  • JIT编译时使用Native Memory,并且JIT的输入(Java字节码)和输出(可执行代码)也都是保存在Native Memory;
  • NIO direct buffer;
  • 线程资源。

JNI内存和引用

  • 在Java代码中,Java对象被存放在JVM的Java Heap,由垃圾回收器(Garbage Collector,即GC)自动回收就可以。
  • 在Native代码中,内存是从Native Memory中分配的,需要根据Native编程规范去操作内存。如:C/C++使用malloc()/new分配内存,需要手动使用free()/delete回收内存。

然而,JNI和上面两者又有些区别

JNI提供了与Java相对应的引用类型(如:jobject、jstring、jclass、jarray、jintArray等),以便Native代码可以通过JNI函数访问到Java对象。

  • 引用所指向的Java对象通常就是存放在Java Heap,
  • 而Native代码持有的引用是存放在Native Memory中。

举个例子,如下代码:

jstring jstr = env->NewStringUTF("Hello World!");
  1. jstring类型是JNI提供的,对应于Java的String类型
  2. JNI函数NewStringUTF()用于构造一个String对象,该对象存放在Java Heap中,同时返回了一个jstring类型的引用。
  3. String对象的引用保存在jstr中,jstr是Native的一个局部变量,存放在Native Memory中

为了避免出现OOM异常和内存泄露,我们在进行JNI开发的时候,需要熟悉它的内存分配和管理。

JNI引用类型

JNI引用类型有三种:Local Reference、Global Reference、Weak Global Reference。

下面分别来介绍一下这三种引用内存分配和管理。

Local Reference

Local Reference生命周期:

  • 在Native Method的执行期开始创建,在Native Method执行完毕切换回Java代码时,JVM发现没有JAVA层引用,Local Reference被JVM回收并释放,所有Local Reference被删除,生命期结束;
  • 或调用DeleteLocalRef可以提前结束其生命期。
  • 局部引用只对当前线程有效,多个线程间不能共享局部引用。
  • 基于谁创建谁销毁的原则,native函数执行完后,局部引用没有被native代码显示删除,那么局部引用在JVM中还是有效的,JVM决定什么时候删除它,和C语言的局部变量含义是不一样的。

每当线程从Java环境切换到Native代码环境时,JVM 会分配一块内存用于创建一个Local Reference Table,这个Table用来存放本次Native Method 执行中创建的所有Local Reference,所以Local Reference不属于Native Code 的局部变量

每当在 Native代码中引用到一个Java对象时,JVM 就会在这个Table中创建一个Local Reference

比如jstring jstr = env->NewStringUTF("Hello World!");,我们调用 NewStringUTF() 在 Java Heap 中创建一个 String 对象后,在 Local Reference Table 中就会相应新增一个 Local Reference。

  • jstr存放在Native Method Stack中,是一个局部变量;

  • 然后通过 Local Reference Table中的 localRef 指向 Heap Mem ,Local Reference Table对我们来说是透明的;

  • Local Reference Table的内存不大,所能存放的Local Reference数量默认16个是有限的,使用不当就会引起OOM异常,注意管理释放;

  • 在Native Method结束时,JVM会自动释放Local Reference,但在开发中如果循环中或其他情况创建大量Local; Reference,应该及时使用DeleteLocalRef()删除不必要的Local Reference,避免Local Reference Table被撑破。

  • Local Reference并不是Native里面的局部变量,局部变量存放在堆栈中,而Local Reference存放在Local Reference Table中。

  • DeleteLocalRef()的参数是一个jobject引用类型,对于一般的基本数据类型(如:jint,jdouble等),是没必要调用该函数删除掉的,但是像jstring、jintArray、jobject这些就需要了。

/**
* 删除localRef所指向的局部引用。
* @localRef localRef:局部引用
*/
voi DeleteLocalRef(jobject localRef);

 

注意Local Reference的生命周期,如果在Native中需要长时间持有一个Java对象,就不能使用将jobject存储在Native,否则在下次使用的时候,即使同一个线程调用,也将会无法使用。

下面是错误的做法:

class MyPeer {
public:
MyPeer(jstring s) {
str_ = s; // Error: stashing a reference without ensuring it’s global.
}
jstring str_;
}; static jlong MyClass_newPeer(JNIEnv* env, jclass) {
jstring local_ref = env->NewStringUTF("hello, world!");
MyPeer* peer = new MyPeer(local_ref);
return static_cast<jlong>(reinterpret_cast<uintptr_t>(peer));
// Error: local_ref is no longer valid when we return, but we've stored it in 'peer'.
} static void MyClass_printString(JNIEnv* env, jclass, jlong peerAddress) {
MyPeer* peer = reinterpret_cast<MyPeer*>(static_cast<uintptr_t>(peerAddress));
// Error: peer->str_ is invalid!
ScopedUtfChars s(env, peer->str_);
std::cout << s.c_str() << std::endl;
}

正确的做法是使用Global Reference,如下:

class MyPeer {
public:
MyPeer(JNIEnv* env, jstring s) {
this->s = env->NewGlobalRef(s);
}
~MyPeer() {
assert(s == NULL);
}
void destroy(JNIEnv* env) {
env->DeleteGlobalRef(s);
s = NULL;
}
jstring s;
};

Global Reference

在理解了Local Reference之后,再来理解Global Reference和Weak Global Reference就简单多了。

Global Reference与Local Reference的区别在于生命周期和作用范围:

  1. Global Reference是通过JNI函数NewGlobalRef()和DeleteGlobalRef()来创建和删除的。
  2. Global Reference具有全局性,可以在多个Native Method调用过程和多线程之间共享其指向的对象,在程序员主动调用DeleteGlobalRef之前,它是一直存在的(GC不会回收其内存)。
/**
* 创建obj参数所引用对象的新全局引用。
* obj参数既可以是全局引用,也可以是局部引用。
* 全局引用通过调用DeleteGlobalRef()来显式撤消。
* @param obj 全局或局部引用。
* @return 返回全局引用。如果系统内存不足则返回 NULL。
*/
jobject NewGlobalRef(jobject obj); /**
* 删除globalRef所指向的全局引用
* @param globalRef 全局引用
*/
void DeleteGlobalRef(jobject globalRef);

Weak Global Reference

Weak Global Reference用NewWeakGlobalRef()和DeleteWeakGlobalRef()进行创建和删除。

它与Global Reference的区别在于该类型的引用随时都可能被GC回收。或在内存紧张时进行回收而被释放。

对于Weak Global Reference而言,可以通过isSameObject()将其与NULL比较,看看是否已经被回收了。

如果返回JNI_TRUE,则表示已经被回收了,需要重新初始化弱全局引用。

/**
* 判断两个引用是否引用同一Java对象。
* @param ref1 Java对象
* @param ref2 Java对象
* @retrun 如果ref1和ref2引用同一Java对象或均为 NULL,则返回 JNI_TRUE。否则返回 JNI_FALSE。
*/
jboolean IsSameObject(jobject ref1, jobject ref2);

Weak Global Reference的回收时机是不确定的,有可能在前一行代码判断它是可用的,后一行代码就被GC回收掉了。

为了避免这事事情发生,JNI官方给出了正确的做法,通过NewLocalRef()获取Weak Global Reference,避免被GC回收。

示例代码如下:

static jobject weakRef = NULL;

JNIEXPORT void JNICALL Java_com_bassy_jnitest_Main_getName(JNIEnv *env, jobject instance) {

jobject localRef;
//We ensure create localRef success
while(weakRef == NULL || (localRef = env->NewLocalRef(weakRef)) == NULL){
//Init weak global reference again
weakRef = env->NewWeakGlobalRef(...)
}
//Process localRef
//...
env->DeleteLocalRef(localRef);
}

相关Tips和优化

  • 在jni_onload初始化全局引用和弱全局引用;

  • jobject默认是local Ref,函数环境消失时会跟随消失

  • C++调用java需要查找类,查找方法,查找方法ID,获取字段或者方法的调用有时候会需要在JVM中完成大量工作,因为字段和方法可能是从超类中继承而来的,为特定类返回的id不会在Jvm进程生存期间发生变化 ,这会让jvm向上遍历类层次结构来找到它们,这是开销很大的操作。

    所以,缓存ID字段是为了降低CPU负载,提高运行速度。

    1. jmethodID/jfielID 和 jobject 没有继承关系,它不是个object,只是个整数,不存在被释放与否的问题,可用全局变量保存。
    2. jclass、jstring是由jobject继承而来的类,所以它是个jobject,需要用全局变量保存。
  • 局部引用管理new出来的对象,注意及时delete。总体原则,注意释放所有对jobject的引用。

  • 不同线程使用JNIEnv*对象,需要AttachCurrentThread将env挂到当前线程,否则无法使用env。

  • 尽量避免频繁调用JNI或者是使用JNI传输大量到数据。

开发相关

二维数组

二维数组具有特殊性在于,可以将它看成一维数组,其中数组的每项内容又是一维数组。

参考

  • 基础知识:
  1. JNI内存管理
  • 开发相关:
  1. Java层与Jni层的数组传递
  2. jbytearray与 C++Byte数组之间的转换
  3. JNI调用时缓存字段和方法 ID
  4. how to cache classId

JNI相关使用记录的更多相关文章

  1. Echarts的相关问题记录与应用

    一.相关问题记录: 1.对图表的div进行隐藏操作,使用hide()或display:none,重新展示时,会造成图表无法获取高度,导致图表的高宽不符合预期: 解决方法:最后调用一下resize()函 ...

  2. piezo film 压电相关信息记录 (2018-05-04 更新)

    piezo film 压电相关信息记录 起因需要使用 Piezo 做一些设计 http://www.te.com.cn/chn-zh/videos/transportation/piezo-film- ...

  3. saltstack 开发相关命令记录

    SALT API开发相关命令记录. 查看当前的salt key信息salt-key -L 测试被控主机的连通性salt '*' test.ping 远程命令执行测试salt '*' cmd.run ' ...

  4. Vue-cli 多页相关配置记录

    Vue-cli 多页相关配置记录 搭建一个顺手的MPA项目脚手架,其实根据项目的不同目录结构和打包配置都可以进行灵活的调整.这次的项目可能是包含各种客户端和管理后台在一起的综合项目所以需要将样式和脚本 ...

  5. JNI相关笔记 [TOC]

    JNI相关笔记 目录 JNI相关笔记 1 生成native code所需要的头文件 2 JNI提供的一些函数和方法 3 局部引用,全局引用,全局弱引用. 4 异常 1 生成native code所需要 ...

  6. docker安装CentOS7及JNI使用相关过程记录

    docker pull centos:centos7(拉取镜像) docker run -itd --name centos-test centos:centos7 (运行容器) docker exe ...

  7. JNI使用问题记录

    此文章包含Android JNI学习过程中的遇到的各种错误记录和学习总结. 1.错误:java.lang.UnsatisfiedLinkError: Native method not found: ...

  8. Yii2的相关学习记录,前后台分离及migrate使用(七)

    最近一直忙其它的(其实是懒!),将<深入理解Yii2>看了一遍,一些当初没明白的稍微明了了点,然后又看yii2的图片上传等处理.富文本.restful什么的,但由于没进行到这里,只看也不管 ...

  9. Yii2的相关学习记录,alert等美化、confirm异步、session中的flash及小部件的使用(六)

    呃,系统自带的alert.confirm等弹出框实在是难看,作为一个颜控,这能忍? 这里我用的是kartik-v/yii2-dialog,这个是基于bootstrap3-dialog这个来做了一些常用 ...

随机推荐

  1. 使用Velero Restic快速完成云原生应用迁移至ACK集群

    本文记录使用Velero Restic快速完成云原生应用迁移至ACK集群的实践过程. 0. 实践步骤概览 (1)创建GKE集群(或自建Kubernetes集群)(2)在GKE集群上部署示例应用Jenk ...

  2. Codeforces 336C

    这题是大一暑假时候做的,当时没有出,直到今天突然觉得应该把没过的题目再做一边,不然真的是越积越多. 现在能够独立做出来真的是难以表达的兴奋,刚开始的时候就觉得 O(30 * 30 * n)的复杂度有点 ...

  3. 你在用 JWT 代替 Session?

    现在,JSON Web Tokens (JWT) 是非常流行的.尤其是 Web 开发领域. 流行 安全 稳定 易用 支持 JSON 所有这些因素,令 JWT 名声大振. 但是,今天我要来说说使用 JW ...

  4. 从零学React Native之08Image组件

    开发过程中, 几乎每个项目都会用到图片. RN就是通过Image组件显示图片.既可以加载网络图片,也可以加载本地资源图片. Image组件必须在样式中声明图片的款和高.如果没有声明,则图片将不会被呈现 ...

  5. python 文本文件的操作

  6. Oracle函数——COALESCE

    COALESCE 含义:COALESCE是一个函数, (expression_1, expression_2, ...,expression_n)依次参考各参数表达式,遇到非null值即停止并返回该值 ...

  7. Data Flow-File Read-详细过程

  8. oracle函数 mod(x,y)

    [功能]返回x除以y的余数 [参数]x,y,数字型表达式 [返回]数字 [示例] select mod(23,8),mod(24,8) from dual; 返回:7,0

  9. es6 set简析

    1.数据结构Set类似于数组,但是成员的值都是唯一的,没有重复的值. var s = new Set(); [,,,,,,].map(x => s.add(x)) for (i of s) {d ...

  10. 「BZOJ2510」弱题

    「BZOJ2510」弱题 这题的dp式子应该挺好写的,我是不会告诉你我开始写错了的,设f[i][j]为操作前i次,取到j小球的期望个数(第一维这么大显然不可做),那么 f[i][j]=f[i-1][j ...