JNI开发之局部引用、全局引用和弱全局引用(三)

本文详细讲述了JNI编程中局部引用表溢出的问题,强调了及时释放局部引用的重要性,介绍了全局引用和弱全局引用的区别,以及如何正确管理引用以防止内存溢出和资源浪费。
摘要由CSDN通过智能技术生成

1、JNI会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。经测试,Android上的JNI局部引用表最大数量是512个。当我们在实现一个本地方法时,可能需要创建大量的局部引用,如果没有及时释放,就有可能导致JNI局部引用表的溢出,所以,在不需要局部引用时就立即调用DeleteLocalRef手动删除。比如,在下面的代码中,本地代码遍历一个特别大的字符串数组,每遍历一个元素,都会创建一个局部引用,当对使用完这个元素的局部引用时,就应该马上手动释放它。

for (i = 0; i < len; i++) {
jstring jstr = (env)->GetObjectArrayElement(env, arr, i);
… /
使用jstr */
(*env)->DeleteLocalRef(env, jstr); // 使用完成之后马上释放
}

2、在编写JNI工具函数时,工具函数在程序当中是公用的,被谁调用你是不知道的。上面newString这个函数演示了怎么样在工具函数中使用完局部引用后,调用DeleteLocalRef删除。不这样做的话,每次调用newString之后,都会遗留两个引用占用空间(elemArray和cls_string,cls_string不用static缓存的情况下)。
3、如果你的本地函数不会返回。比如一个接收消息的函数,里面有一个死循环,用于等待别人发送消息过来while(true) { if (有新的消息) { 处理之。。。。} else { 等待新的消息。。。}}。如果在消息循环当中创建的引用你不显示删除,很快将会造成JVM局部引用表溢出。
4、局部引用会阻止所引用的对象被GC回收。比如你写的一个本地函数中刚开始需要访问一个大对象,因此一开始就创建了一个对这个对象的引用,但在函数返回前会有一个大量的非常复杂的计算过程,而在这个计算过程当中是不需要前面创建的那个大对象的引用的。但是,在计算的过程当中,如果这个大对象的引用还没有被释放的话,会阻止GC回收这个对象,内存一直占用者,造成资源的浪费。所以这种情况下,在进行复杂计算之前就应该把引用给释放了,以免不必要的资源浪费。

/* 假如这是一个本地方法实现 /
JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv env, jobject this)
{
lref = … /
lref引用的是一个大的Java对象 /
… /
在这里已经处理完业务逻辑后,这个对象已经使用完了 /
(env)->DeleteLocalRef(env, lref); / 及时删除这个对这个大对象的引用,GC就可以对它回收,并释放相应的资源
/
lengthyComputation(); /
在里有个比较耗时的计算过程 /
return; /
计算完成之后,函数返回之前所有引用都已经释放 */
}

管理局部引用

JNI提供了一系列函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI规范指出,任何实现JNI规范的JVM,必须确保每个本地函数至少可以创建16个局部引用(可以理解为虚拟机默认支持创建16个局部引用)。实际经验表明,这个数量已经满足大多数不需要和JVM中内部对象有太多交互的本地方函数。如果需要创建更多的引用,可以通过调用EnsureLocalCapacity函数,确保在当前线程中创建指定数量的局部引用,如果创建成功则返回0,否则创建失败,并抛出OutOfMemoryError异常。EnsureLocalCapacity这个函数是1.2以上版本才提供的,为了向下兼容,在编译的时候,如果申请创建的局部引用超过了本地引用的最大容量,在运行时JVM会调用FatalError函数使程序强制退出。在开发过程当中,可以为JVM添加-verbose:jni参数,在编译的时如果发现本地代码在试图申请过多的引用时,会打印警告信息提示我们要注意。在下面的代码中,遍历数组时会获取每个元素的引用,使用完了之后不手动删除,不考虑内存因素的情况下,它可以为这种创建大量的局部引用提供足够的空间。由于没有及时删除局部引用,因此在函数执行期间,会消耗更多的内存。

/处理函数逻辑时,确保函数能创建len个局部引用/
if((*env)->EnsureLocalCapacity(env,len) != 0) {
… /申请len个局部引用的内存空间失败 OutOfMemoryError/
return;
}
for(i=0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
// … 使用jstr字符串
/这里没有删除在for中临时创建的局部引用/
}

另外,除了EnsureLocalCapacity函数可以扩充指定容量的局部引用数量外,我们也可以利用Push/PopLocalFrame函数对创建作用范围层层嵌套的局部引用。例如,我们把上面那段处理字符串数组的代码用Push/PopLocalFrame函数对重写:

#define N_REFS … /最大局部引用数量/
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
… /内存溢出/
}
jstring jstr = (env)->GetObjectArrayElement(env, arr, i);
… /
使用jstr */
(*env)->PopLocalFrame(env, NULL);
}

PushLocalFrame为当前函数中需要用到的局部引用创建了一个引用堆栈,(如果之前调用PushLocalFrame已经创建了Frame,在当前的本地引用栈中仍然是有效的)每遍历一次调用(*env)->GetObjectArrayElement(env, arr, i);返回一个局部引用时,JVM会自动将该引用压入当前局部引用栈中。而PopLocalFrame负责销毁栈中所有的引用。这样一来,Push/PopLocalFrame函数对提供了对局部引用生命周期更方便的管理,而不需要时刻关注获取一个引用后,再调用DeleteLocalRef来释放引用。在上面的例子中,如果在处理jstr的过程当中又创建了局部引用,则PopLocalFrame执行时,这些局部引用将全都会被销毁。在调用PopLocalFrame销毁当前frame中的所有引用前,如果第二个参数result不为空,会由result生成一个新的局部引用,再把这个新生成的局部引用存储在上一个frame中。请看下面的示例:

// 函数原型
jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result);

jstring other_jstr;
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
… /内存溢出/
}
jstring jstr = (env)->GetObjectArrayElement(env, arr, i);
… /
使用jstr */
if (i == 2) {
other_jstr = jstr;
}
other_jstr = (*env)->PopLocalFrame(env, other_jstr); // 销毁局部引用栈前返回指定的引用
}

还要注意的一个问题是,局部引用不能跨线程使用,只在创建它的线程有效。不要试图在一个线程中创建局部引用并存储到全局引用中,然后在另外一个线程中使用。

全局引用

全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,也会阻止它所引用的对象被GC回收。与局部引用创建方式不同的是,只能通过NewGlobalRef函数创建。下面这个版本的newString演示怎么样使用一个全局引用:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
// …
jstring jstr = NULL;
static jclass cls_string = NULL;
if (cls_string == NULL) {
jclass local_cls_string = (*env)->FindClass(env, “java/lang/String”);
if (cls_string == NULL) {
return NULL;
}

// 将java.lang.String类的Class引用缓存到全局引用当中
cls_string = (*env)->NewGlobalRef(env, local_cls_string);

// 删除局部引用
(*env)->DeleteLocalRef(env, local_cls_string);

// 再次验证全局引用是否创建成功
if (cls_string == NULL) {
return NULL;
}
}

// …
return jstr;
}

弱全局引用

弱全局引用使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放。下面简称弱引用。与全局引用类似,弱引用可以跨方法、线程使用。但与全局引用很重要不同的一点是,弱引用不会阻止GC回收它引用的对象。在newString这个函数中,我们也可以使用弱引用来存储String的Class引用,因为java.lang.String这个类是系统类,永远不会被GC回收。当本地代码中缓存的引用不一定要阻止GC回收它所指向的对象时,弱引用就是一个最好的选择。假设,一个本地方法mypkg.MyCls.f需要缓存一个指向类mypkg.MyCls2的引用,如果在弱引用中缓存的话,仍然允许mypkg.MyCls2这个类被unload,因为弱引用不会阻止GC回收所引用的对象。请看下面的代码段:

JNIEXPORT void JNICALL
Java_mypkg_MyCls_f(JNIEnv *env, jobject self)
{
static jclass myCls2 = NULL;
if (myCls2 == NULL)
{
jclass myCls2Local = (env)->FindClass(env, “mypkg/MyCls2”);
if (myCls2Local == NULL)
{
return; /
没有找到mypkg/MyCls2这个类 /
}
myCls2 = NewWeakGlobalRef(env, myCls2Local);
if (myCls2 == NULL)
{
return; /
内存溢出 /
}
}
… /
使用myCls2的引用 */
}

我们假设MyCls和MyCls2有相同的生命周期(例如,他们可能被相同的类加载器加载),因为弱引用的存在,我们不必担心MyCls和它所在的本地代码在被使用时,MyCls2这个类出现先被unload,后来又会preload的情况。当然,如果真的发生这种情况时(MyCls和MyCls2此时的生命周期不同),我们在使用弱引用时,必须先检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被GC给unload的类对象。下面马上告诉你怎样检查弱引用是否活动,即引用的比较。

引用比较

给定两个引用(不管是全局、局部还是弱全局引用),我们只需要调用IsSameObject来判断它们两个是否指向相同的对象。例如:(*env)->IsSameObject(env, obj1, obj2),如果obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)。有一个特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null对象。如果obj是一个局部或全局引用,使用(*env)->IsSameObject(env, obj, NULL) 或者 obj == NULL 来判断obj是否指向一个null对象即可。但需要注意的是,IsSameObject用于弱全局引用与NULL比较时,返回值的意义是不同于局部引用和全局引用的:

jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);
jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);
// … 业务逻辑处理
jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL);

在上面的IsSameObject调用中,如果g_obj_ref指向的引用已经被回收,会返回JNI_TRUE,如果wobj仍然指向一个活动对象,会返回JNI_FALSE。

释放全局引用

每一个JNI引用被建立时,除了它所指向的JVM中对象的引用需要占用一定的内存空间外,引用本身也会消耗掉一个数量的内存空间。作为一个优秀的程序员,我们应该对程序在一个给定的时间段内使用的引用数量要十分小心。短时间内创建大量而没有被立即回收的引用很可能就会导致内存溢出。

当我们的本地代码不再需要一个全局引用时,应该马上调用DeleteGlobalRef来释放它。如果不手动调用这个函数,即使这个对象已经没用了,JVM也不会回收这个全局引用所指向的对象。
同样,当我们的本地代码不再需要一个弱全局引用时,也应该调用DeleteWeakGlobalRef来释放它,如果不手动调用这个函数来释放所指向的对象,JVM仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。

管理引用的规则

前面对三种引用已做了一个全面的介绍,下面来总结一下引用的管理规则和使用时的一些注意事项,使用好引用的目的就是为了减少内存使用和对象被引用保持而不能释放,造成内存浪费。所以在开发当中要特别小心!
通常情况下,有两种本地代码使用引用时要注意:
1、 直接实现Java层声明的native函数的本地代码
当编写这类本地代码时,要当心不要造成全局引用和弱引用的累加,因为本地方法执行完毕后,这两种引用不会被自动释放。
2、被用在任何环境下的工具函数。例如:方法调用、属性访问和异常处理的工具函数等。
编写工具函数的本地代码时,要当心不要在函数的调用轨迹上遗漏任何的局部引用,因为工具函数被调用的场合和次数是不确定的,一量被大量调用,就很有可能造成内存溢出。所以在编写工具函数时,请遵守下面的规则:
1> 一个返回值为基本类型的工具函数被调用时,它决不能造成局部、全局、弱全局引用被回收的累加
2> 当一个返回值为引用类型的工具函数被调用时,它除了返回的引用以外,它决不能造成其它局部、全局、弱引用的累加
对于工具函数来说,为了使用缓存技术而创建一些全局引用或者弱全局引用是正常的。如果一个工具函数返回的是一个引用,我们应该写好注释详细说明返回引用的类型,以便于使用者更好的管理它们。下面的代码中,频繁地调用工具函数GetInfoString,我们需要知道GetInfoString返回引用的类型是什么,以便于每次使用完成后调用相应的JNI函数来释放掉它。

while (JNI_TRUE) {
jstring infoString = GetInfoString(info);
… /* 处理infoString /
??? /
使用完成之后,调用DeleteLocalRef、DeleteGlobalRef、DeleteWeakGlobalRef哪一个函数来释放这个引用呢?*/
}

函数NewLocalRef有时被用来确保一个工具函数返回一个局部引用。我们改造一下newString这个函数,演示一下这个函数的用法。下面的newString是把一个被频繁调用的字符串“CommonString”缓存在了全局引用里:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
{
static jstring result;
/* 使用wstrncmp函数比较两个Unicode字符串 /
if (wstrncmp(“CommonString”, chars, len) == 0)
{
/
将"CommonString"这个字符串缓存到全局引用中 /
static jstring cachedString = NULL;
if (cachedString == NULL)
{
/
先创建"CommonString"这个字符串 /
jstring cachedStringLocal = …;
/
然后将这个字符串缓存到全局引用中 */
cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);
}
// 基于全局引用创建一个局引用返回,也同样会阻止GC回收所引用的这个对象,因为它们指向的是同一个对象
return (*env)->NewLocalRef(env, cachedString);
}

return result;
}

在管理局部引用的生命周期中,Push/PopLocalFrame是非常方便且安全的。我们可以在本地函数的入口处调用PushLocalFrame,然后在出口处调用PopLocalFrame,这样的话,在函数内任何位置创建的局部引用都会被释放。而且,这两个函数是非常高效的,强烈建议使用它们。需要注意的是,如果在函数的入口处调用了PushLocalFrame,记住要在函数所有出口(有return语句出现的地方)都要调用PopLocalFrame。在下面的代码中,对PushLocalFrame的调用只有一次,但调用PopLocalFrame确有多次,当然你也可以使用goto语句来统一处理。

jobject f(JNIEnv *env, …)
{
jobject result;
if ((env)->PushLocalFrame(env, 10) < 0)
{
/
调用PushLocalFrame获取10个局部引用失败,不需要调用PopLocalFrame /
return NULL;
}

result = …; // 创建局部引用result
if (…)
{
/
返回前先弹出栈顶的frame */
result = (*env)->PopLocalFrame(env, result);
return result;
}

result = (env)->PopLocalFrame(env, result);
/
正常返回 */
return result;
}

上面的代码同样演示了函数PopLocalFrame的第二个参数的用法,局部引用result一开始在PushLocalFrame创建在当前frame里面,而把result传入PopLocalFrame中时,PopLocalFrame在弹出当前的frame前,会由result生成一个新的局部引用,再将这个新生成的局部引用存储在上一个frame当中。

原文链接:https://blog.csdn.net/xyang81/article/details/44657385
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

答应大伙的备战金三银四,大厂面试真题来啦!

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

4581420)]

腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析

[外链图片转存中…(img-ys1dGQTA-1713384581420)]

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值