这篇文章比较偏理论,详细介绍了在编写本地代码时三种引用的使用场景和注意事项。可能看起来有点枯燥,但引用是在JNI中最容易出错的一个点,如果使用不当,容易使程序造成内存溢出,程序崩溃等现象。所以讲得比较细,有些地方看起来可能比较啰嗦,还请轻啪!《Android JNI局部引用表溢出:local reference table overflow (max=512)》这篇文章是一个JNI引用使用不当造成引用表溢出,最终导致程序崩溃的例子。建议看完这篇文章之后,再去看。
做Java的朋友都知道,在编码的过程当中,内存管理这一块完全是透明的。new一个类的实例时,只知道创建完这个类的实例之后,会返回这个实例的一个引用,然后就可以拿着这个引用访问它的所有数据成员了(属性、方法)。完全不用管JVM内部是怎么实现的,如何为新创建的对象来申请内存,也不用管对象使用完之后内存是怎么释放的,只需知道有一个垃圾回器在帮忙管理这些事情就OK的了。有经验的朋友也许知道启动一个Java程序,如果没有手动创建其它线程,默认会有两个线程在跑,一个是main线程,另一个就是GC线程(负责将一些不再使用的对象回收)。如果你曾经是做Java的然后转去做C++,会感觉很“蛋疼”,在C++中new一个对象,使用完了还要做一次delete操作,malloc一次同样也要调用free来释放相应的内存,否则你的程序就会有内存泄露了。而且在C/C++中内存还分栈空间和堆空间,其中局部变量、函数形参变量、for中定义的临时变量所分配的内存空间都是存放在栈空间(而且还要注意大小的限制),用new和malloc申请的内存都存放在堆空间。。。但C/C++里的内存管理还远远不止这些,这些只是最基础的内存管理常识。做Java的童鞋听到这些肯定会偷乐了,咱写Java的时候这些都不用管,全都交给GC就万事无优了。手动管理内存虽然麻烦,而且需要特别细心,一不小心就有可能造成内存泄露和野指针访问等程序致命的问题,但凡事都有利弊,手动申请和释放内存对程序的掌握比较灵活,不会受到平台的限制。比如我们写Android程序的时候,内存使用就受Dalivk虚拟机的限制,从最初版本的16~24M,到后来的32M到64M,可能随着以后移动设备物理内存的不大扩大,后面的Android版本内存限制可能也会随着提高。但在C/C++这层,就完全不受虚拟机的限制了。比如要在Android中要存储一张超高清的图片,刚好这张图片的大小超过了Dalivk虚拟机对每个应用的内存大小限制,Java此时就显得无能为力了,但在C/C++看来就是小菜一碟了,malloc(1024*1024*50),要多少内存,您说个数。。。C/C++程序员得意的说道~~Java不是说是一门纯面象对象的语言吗,所以除了基本数据类型外,其它任何类型所创建的对象,JVM所申请的内存都存在堆空间。上面提高到了GC,是负责回收不再使用的对象,它的全称是Garbage Collection,也就是所谓的垃圾回收。JVM会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收。那么哪些对象会被认为是不再使用,并且可以被回收的呢?我们来看下面二张图:(注:图摘自博主郭霖的《Android最佳性能实践(二)——分析内存的使用情况》)
上图当中,每个蓝色的圆圈就代表一个内存当中的对象,而圆圈之间的箭头就是它们的引用关系。这些对象有些是处于活动状态的,而有些就已经不再被使用了。那么GC操作会从一个叫作Roots的对象开始检查,所有它可以访问到的对象就说明还在使用当中,应该进行保留,而其它的对象就表示已经不再被使用了,如下图所示:
可以看到,目前所有黄色的对象都处于活动状态,仍然会被系统继续保留,而蓝色的对象就会在GC操作当中被系统回收掉了,这就是JVM执行一次GC的简单流程。
上面说的废话好像有点多哈,下面进入正题。通过上面的讨论,大家都知道,如果一个Java对象没有被其它成员变量或静态变量所引用的话,就随时有可能会被GC回收掉。所以我们在编写本地代码时,要注意从JVM中获取到的引用在使用时被GC回收的可能性。由于本地代码不能直接通过引用操作JVM内部的数据结构,要进行这些操作必须调用相应的JNI接口来间接操作所引用的数据结构。JNI提供了和Java相对应的引用类型,供本地代码配合JNI接口间接操作JVM内部的数据内容使用。如:jobject、jstring、jclass、jarray、jintArray等。因为我们只通过JNI接口操作JNI提供的引用类型数据结构,而且每个JVM都实现了JNI规范相应的接口,所以我们不必担心特定JVM中对象的存储方式和内部数据结构等信息,我们只需要学习JNI中三种不同的引用即可。
由于Java程序运行在虚拟机中的这个特点,在Java中创建的对象、定义的变量和方法,内部对象的数据结构是怎么定义的,只有JVM自己知道。如果我们在C/C++中想要访问Java中对象的属性和方法时,是不能够直接操作JVM内部Java对象的数据结构的。想要在C/C++中正确的访问Java的数据结构,JVM就必须有一套规则来约束C/C++与Java互相访问的机制,所以才有了JNI规范,JNI规范定义了一系列接口,任何实现了这套JNI接口的Java虚拟机,C/C++就可以通过调用这一系列接口来间接的访问Java中的数据结构。比如前面文章中学习到的常用JNI接口有:GetStringUTFChars(从Java虚拟机中获取一个字符串)、ReleaseStringUTFChars(释放从JVM中获取字符串所分配的内存空间)、NewStringUTF、GetArrayLength、GetFieldID、GetMethodID、FindClass等。
三种引用简介及区别
在JNI规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。区别如下:
1、局部引用:通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放。(*env)->DeleteLocalRef(env,local_ref)
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
jcharArray charArr = (*env)->NewCharArray(env, len);
jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj); // 通过NewLocalRef函数创建
...
- 1
- 2
- 3
- 4
- 5
2、全局引用:调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef手动释放(*env)->DeleteGlobalRef(env,g_cls_string);
3、 弱全局引用:调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef手动释放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)