加入收藏 | 设为首页 | 会员中心 | 我要投稿 安卓应用网 (https://www.0791zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程开发 > Java > 正文

解析Java的JNI编程中的对象引用与内存泄漏问题

发布时间:2020-05-23 03:39:30 所属栏目:Java 来源:互联网
导读:JNI,JavaNativeInterface,是nativecode的编程接口。JNI使Java代码程序可以与nativecode交互――在Java程序中调用nativecode;在nativecode中嵌入Java虚拟机调用Java的代码。

JNI,Java Native Interface,是 native code 的编程接口。JNI 使 Java 代码程序可以与 native code 交互――在 Java 程序中调用 native code;在 native code 中嵌入 Java 虚拟机调用 Java 的代码。
JNI 编程在软件开发中运用广泛,其优势可以归结为以下几点:
利用 native code 的平台相关性,在平台相关的编程中彰显优势。
对 native code 的代码重用。
native code 底层操作,更加高效。
然而任何事物都具有两面性,JNI 编程也同样如此。程序员在使用 JNI 时应当认识到 JNI 编程中如下的几点弊端,扬长避短,才可以写出更加完善、高性能的代码:
从 Java 环境到 native code 的上下文切换耗时、低效。
JNI 编程,如果操作不当,可能引起 Java 虚拟机的崩溃。
JNI 编程,如果操作不当,可能引起内存泄漏。
JAVA 中的内存泄漏
JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM 中 Java Heap 的内存泄漏;JVM 内存中 native memory 的内存泄漏。

局部和全局引用

JNI将实例、数组类型暴露为不透明的引用。native代码从不会直接检查一个不透明的引用指针的上下文,而是通过使用JNI函数来访问由不透明的引用所指向的数据结构。因为只处理不透明的引用,这样就不需要担心不同的java VM实现而导致的不同的内部对象的布局。然而,还是有必要了解一下JNI中不同种类的引用:
1)JNI 支持3中不透明的引用:局部引用、全局引用和弱全局引用。
2)局部和全局引用,有着各自不同的生命周期。局部引用能够被自动释放,而全局引用和若全局引用在被程序员释放之前,是一直有效的。
3)一个局部或者全局引用,使所提及的对象不能被垃圾回收。而弱全局引用,则允许提及的对象进行垃圾回收。
4)不是所有的引用都可以在所有上下文中使用的。例如:在一个创建返回引用native方法之后,使用一个局部引用,这是非法的。

那么到底什么是局部引用,什么事全局引用,它们有什么不同?

局部引用

多数JNI函数都创建局部引用。例如JNI函数NewObject创建一个实例,并且返回一个指向该实例的局部引用。

局部引用只在创建它的native方法的动态上下文中有效,并且只在native方法的一次调用中有效。所有局部引用只在一个native方法的执行期间有效,在该方法返回时,它就被回收。

在native方法中使用一个静态变量来保存一个局部引用,以便在随后的调用中使用该局部引用,这种方式是行不通的。例如以下例子,误用了局部引用:
/* This code is illegal */ 
jstring 

MyNewString(JNIEnv *env,jchar *chars,jint len) 
{ 
  static jclass stringClass = NULL; 
  jmethodID cid; 
  jcharArray elemArr; 
  jstring result; 
  if (stringClass == NULL) { 
    stringClass = (*env)->FindClass(env,"java/lang/String"); 
    if (stringClass == NULL) { 
      return NULL; /* exception thrown */ 
    } 
  } 
  /* It is wrong to use the cached stringClass here,because it may be invalid. */ 
  cid = (*env)->GetMethodID(env,stringClass,"<init>","([C)V"); 
  ... 
  elemArr = (*env)->NewCharArray(env,len); 
  ... 
  result = (*env)->NewObject(env,cid,elemArr); 
  (*env)->DeleteLocalRef(env,elemArr); 
  return result; 
} 

这种保存局部引用的方式是不正确的,因为FindClass()返回的是对java.lang.String的局部引用。这是因为,在native代码从MyNewString返回退出时,VM 会释放所有局部引用,包括存储在stringClass变量中的指向类对象的引用。这样当再次后继调用MyNewString时,可能会访问非法地址,导致内存被破坏,或者系统崩溃。

局部引用失效,有两种方式:‘
1)系统会自动释放局部变量。
2)程序员可以显示地管理局部引用的生命周期,例如调用DeleteLocalRef。

一个局部引用可能在被摧毁之前,被传给多个native方法。例如,MyNewString中,返回一个由NewObject创建的字符串引用,它将由NewObject的调用者来决定是否释放该引用。而在以下代码中:

JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env,jobject this) { 
   char *c_str = ...<pre name="code" class="cpp">   ... <pre name="code" class="cpp">return MyNewString(c_str);<pre name="code" class="cpp">} 

在VM接收到来自Java_C_f的局部引用以后,将基础字符串对象传递给ava_C_f的调用者,然后摧毁原本由MyNewString中调用的JNI函数NewObject所创建的局部引用。

局部对象只属于创建它们的线程,只在该线程中有效。一个线程想要调用另一个线程创建的局部引用是不被允许的。将一个局部引用保存到全局变量中,然后在其它线程中使用它,这是一种错误的编程。

全局引用

在一个native方法被多次调用之间,可以使用一个全局引用跨越它们。一个全局引用可以跨越多个线程,并且在被程序员释放之前,一致有效。和局部引用一样,全局引用保证了所引用的对象不会被垃圾回收。

和局部引用不一样(局部变量可以由多数JNI函数创建),全局引用只能由一个JNI函数创建(NewGlobalRef)。下面是一个使用全局引用版本的MyNewString:
/* This code is OK */ 
jstring 

MyNewString(JNIEnv *env,jint len) 
{ 
  static jclass stringClass = NULL; 
  ... 
  if (stringClass == NULL) { 
    jclass localRefCls = 
      (*env)->FindClass(env,"java/lang/String"); 
    if (localRefCls == NULL) { 
      return NULL; /* exception thrown */ 
    } 
    /* Create a global reference */ 
    stringClass = (*env)->NewGlobalRef(env,localRefCls); 
    /* The local reference is no longer useful */ 
    (*env)->DeleteLocalRef(env,localRefCls); 
    /* Is the global reference created successfully? */ 
    if (stringClass == NULL) { 
      return NULL; /* out of memory exception thrown */ 
    } 
  } 
  ... 
} 


弱全局引用


弱全局引用是在java 2 SDK1.2才出现的。它由NewGolableWeakRef函数创建,并且被DeleteGloablWeakRef函数摧毁。和全局引用一样,它可以跨native方法调用,也可以跨越不同线程。但是和全局引用不同的是,它不阻止对基础对象的垃圾回收。下面是弱全局引用版的MyNewString:

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; /* can't find class */ 
    } 
    myCls2 = NewWeakGlobalRef(env,myCls2Local); 
    if (myCls2 == NULL) { 
      return; /* out of memory */ 
    } 
  } 
  ... /* use myCls2 */ 
} 

弱全局引用在一个被native代码缓存着的引用不想阻止基础对象被垃圾回收时,非常有用。如以上例子,mypkg.MyCls.f需要缓存mypkg.MyCls2的引用。而通过将mypkg.MyCls2缓存到弱引用中,能够实现MyCls2类依旧可以被卸载。


上面代码中,我们假设了MyCls类和MyCls2类的生命周期是相同的(例如,在同一个类中被加载、卸载)。所以没有考虑MyCls2被卸载了,然后在类MyCls和native方法的实现Java_mypkg_MyCls_f还要被继续使用时,再被重新加载起来的情况。针对于这个MyCls2类可能被卸载再加载的情况,在使用时,需要检查该弱全局引用是否还有效。如何检查,这将在下面提到。

比较引用

可以用JNI函数IsSameObject来检查给定的两个局部引用、全局引用或者弱全局引用,是否指向同一个对象。
(*env)->IsSameObject(env,obj1,obj2) 
返回值为:
JNI_TRUE,表示两个对象一致,是同一个对象。
JNI_FALSE,表示两个对象不一致,不是同一个对象。


在java VM中NULL是null的引用。
如果一个对象obj是局部引用或者全局引用,则可以这样来检查它是否指向null对象:

(*env)->IsSameObject(env,obj,NULL) 

或者:

NULL == obj 


而对于弱全局引用,以上规则需要改变一下:
我们可以用这个函数来判断一个非0弱全局引用wobj所指向的对象是否仍旧存活着(依旧有效)。

(*env)->IsSameObject(env,wobj,NULL) 

返回值:
JNI_TRUE,表示对象已经被回收了。
JNI_FALSE,表示wobj指向的对象,依旧有效。

 释放引用
除了引用的对象要占用内存,每个JNI引用本身也会消耗一定内存。作为一个JNI程序员,应该对在一段给定的时间里,程序会用到的引用的个数,做到心中有数。特别是,尽管程序所创建的局部引用最终会被VM会被自动地释放,仍旧需要知道在程序在执行期间的任何时刻,创建的局部引用的上限个数。创建过多的引用,即便他们是瞬间、短暂的,也会导致内存耗尽。

释放局部引用
多数情况下,在执行一个native方法时,你不需要担心局部引用的释放,java VM会在native方法返回调用者的时候释放。然而有时候需要JNI程序员显示的释放局部引用,来避免过高的内存使用。那么什么时候需要显示的释放呢,且看一下情景:
1)在单个native方法调用中,创建了大量的局部引用。这可能会导致JNI局部引用表溢出。此时有必要及时地删除那些不再被使用的局部引用。例如以下代码,在该循环中,每次都有可能创建一个巨大的字符串数组。在每个迭代之后,native代码需要显示地释放指向字符串元素的局部引用:

for (i = 0; i < len; i++) { 
  jstring jstr = (*env)->GetObjectArrayElement(env,arr,i); 
  ... /* process jstr */ 
  (*env)->DeleteLocalRef(env,jstr); 
} 

2)你可能要创建一个工具函数,它会被未知的上下文调用。例如之前提到到MyNewString这个例子,它在每次返回调用者欠,都及时地将局部引用释放。


3)native方法,可能不会返回(例如,一个可能进入无限事件分发的循环中的方法)。此时在循环中释放局部引用,是至关重要的,这样才能不会无限期地累积,进而导致内存泄露。

(编辑:安卓应用网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读