程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 解析Java的JNI編程中的對象援用與內存洩露成績

解析Java的JNI編程中的對象援用與內存洩露成績

編輯:關於JAVA

解析Java的JNI編程中的對象援用與內存洩露成績。本站提示廣大學習愛好者:(解析Java的JNI編程中的對象援用與內存洩露成績)文章只能為提供參考,不一定能成為您想要的結果。以下是解析Java的JNI編程中的對象援用與內存洩露成績正文


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, stringClass, 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, jchar *chars, 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辦法,能夠不會前往(例如,一個能夠進入無窮事宜分發的輪回中的辦法)。此時在輪回中釋放部分援用,是相當主要的,如許能力不會無窮期地積累,進而招致內存洩漏。


4)native辦法能夠拜訪一個偉大的對象,是以,創立了一個指向該對象的部分援用。native辦法在前往挪用者之前,除拜訪對象以外,還履行了額定的盤算。指向這個年夜對象的部分援用,將會包括該對象,以防被渣滓收受接管。這個景象會連續到native辦法前往到挪用者時,即使這個對象不會再被應用,也照舊會受掩護。在以下例子中,因為在lengthyComputation()前,顯示地挪用了DeleteLocalRef,所以渣滓收受接管器無機會可以釋放lref所指向的對象。

/* A native method implementation */ 
JNIEXPORT void JNICALL 
Java_pkg_Cls_func(JNIEnv *env, jobject this) 
{ 
  lref = ...       /* a large Java object */ 
  ...           /* last use of lref */ 
  (*env)->DeleteLocalRef(env, lref); 
  lengthyComputation();  /* may take some time */ 
  return;         /* all local refs are freed */ 
} 

這個情況的本質,就是許可法式在native辦法履行時代,java的渣滓收受接管機制無機會收受接管native代碼不在拜訪的對象。

治理部分援用
不曉得java 7怎樣樣了,應當更壯大吧,有時光,去看看,這裡且依照java2的特征來吧。
SDK1.2中供給了一組額定的函數來治理部分援用的性命周期。他們是EnsureLocalCapacity、NewLocalRef、PushLocalFram和PopLocalFram。
JNI的標准請求VM可以主動確保每一個native辦法可以創立至多16個部分援用。經歷顯示,假如native辦法中未包括和java VM的對象停止龐雜的相互操作,這個容量對年夜多半native辦法而言,曾經足夠了。假如,湧現這還不敷的情形,須要創立更多的部分援用,那末native辦法可以挪用EnsureLocalCapacity來包管這些部分援用有足夠的空間。

/* The number of local references to be created is equal to 
  the length of the array. */ 
if ((*env)->EnsureLocalCapacity(env, len)) < 0) {   ... /* out of memory */ 
} for (i = 0; i < len; i++) { 
  jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); 
  ... /* process jstr */   /* DeleteLocalRef is no longer necessary */ } 

如許做,所消費的內存,天然就有能夠比之前的版原來的多。


別的,PushLocalFram\PopLocalFram函數許可法式員創立嵌套感化域的部分援用。以下代碼:

#define N_REFS ... /* the maximum number of local references 
 used in each iteration */ 
for (i = 0; i < len; i++) { 
  if ((*env)->PushLocalFrame(env, N_REFS) < 0) { 
    ... /* out of memory */ 
  } 
  jstr = (*env)->GetObjectArrayElement(env, arr, i); 
  ... /* process jstr */ 
  (*env)->PopLocalFrame(env, NULL); 
} 

PushLocalFram為指定命目標部分援用,創立一個新的感化域,PopLocalFram摧毀最下層的感化域,而且釋放該域中的一切部分援用。


應用這兩個函數的利益是它們可以治理部分援用的性命周期,而不需關系在履行進程中能夠被創立的每一個零丁部分援用。例子中,假如處置jstr的進程,創立了額定的部分援用,它們也會在PopLocalFram以後被立刻釋放。


NewLocalRef函數,在你寫一個對象函數時,異常有效。這個會鄙人面章節——治理援用的規矩,詳細剖析。

native代碼能夠會創立超越16個部分援用的規模,也能夠將他們保留在PushLocalFram或許EnsureLocalCapacity挪用,VM會為部分援用分派所須要的內存。但是,這些內存能否足夠,是沒有包管的。假如內存分派掉敗,虛擬機將會加入。

 釋放全局援用


在native代碼不再須要拜訪一個全局援用的時刻,應當挪用DeleteGlobalRef來釋放它。假如挪用這個函數掉敗,Java VM將不會收受接管對應的對象。


在native代碼不在須要拜訪一個弱全局援用的時刻,應當挪用DeleteWeakGlobalRef來釋放它。假如挪用這個函數掉敗了,java VM 仍然將會收受接管對應的底層對象,然則,不會收受接管這個弱援用自己所消費失落的內存。


 治理援用的規矩
治理援用的目標是為了消除不須要的內存占用和對象保存。

整體來講,只要兩品種型的native代碼:直接完成native辦法的函數,在二進制高低文中被應用的對象函數。

在寫native辦法的完成的時刻,須要小心在輪回中過度創立部分援用,和在native辦法中被創立的,卻不前往給挪用者的部分援用。在native辦法辦法前往後還留有16個部分援用在應用中,將它們交給java VM來釋放,這是可以接收的。然則native辦法的挪用,不該該惹起全局援用和弱全局援用的積累。應為這些援用不會在native辦法返後被主動地釋放。


在寫對象函數的時刻,必需要留意不克不及洩漏任何部分援用或許超越該函數以外的履行。由於一個對象函數,能夠在乎料以外的高低文中,被一直的反復挪用。任何不須要的援用創立都有能夠招致內存洩漏。
1)當一個前往一個基本類型的對象函數被挪用,它必需應當沒有部分援用、若全局援用的積累。
2)當一個前往一個援用類型的對象函數被挪用,它必需應當沒有部分、全局或若全局援用的積累,除要被作為前往值的援用。


一個對象函數以捕捉為目標創立一些全局或許弱全局援用,這是可接收的,由於只要在最開端的時刻,才會創立這些援用。


假如一個對象函數前往一個援用,你應當使前往的援用的類型(例如部分援用、全局援用)作為函數標准的一部門。它應當持之以恆,而不是有時刻前往一個部分援用,有時刻卻前往一個全局援用。挪用者須要曉得對象函數前往的援用的類型,以便准確地治理本身的JNI援用。以下代碼反復地挪用一個對象對象函數(GetInfoString)。我們須要曉得GetInfoString前往的援用的類型,以便釋放該援用:

while (JNI_TRUE) { 
  jstring infoString = GetInfoString(info); 
  ... /* process infoString */ 
  ??? /* we need to call DeleteLocalRef, DeleteGlobalRef, 
 or DeleteWeakGlobalRef depending on the type of 
 reference returned by GetInfoString. */ 
} 

在java2 SDK1.2中,NewLocalRef函數可以用來包管一個對象函數一向前往一個部分援用。為了解釋這個成績,我們對MyNewString做一些修改,它緩存了一個被頻仍要求的字符串(“CommonString”)到全局援用:

jstring 

MyNewString(JNIEnv *env, jchar *chars, jint len) 
{ 
  static jstring result; 
  /* wstrncmp compares two Unicode strings */ 
  if (wstrncmp("CommonString", chars, len) == 0) { 
    /* refers to the global ref caching "CommonString" */ 
    static jstring cachedString = NULL; 
    if (cachedString == NULL) { 
      /* create cachedString for the first time */ 
      jstring cachedStringLocal = ... ; 
      /* cache the result in a global reference */ 
      cachedString = 
        (*env)->NewGlobalRef(env, cachedStringLocal); 
    } 
    return (*env)->NewLocalRef(env, cachedString); 
  } 
  ... /* create the string as a local reference and store in 
 result as a local reference */ 
  return result; 
} 

正常的流程前往的時刻部分援用。就像之前說明的那樣,我們必需將緩存字符保留到一個全局援用中,如許便可以在多個線程中挪用native辦法時,都能拜訪它。

return (*env)->NewLocalRef(env, cachedString); 

這條語句,創立了一個部分援用,它指向了緩存在全局援用的指向的同一對象。作為和挪用者的商定的一部門,MyNewString老是前往一個部分援用。


PushLocalFram、PopLocalFram函數用來治理部分援用的性命周期特殊得便利。只須要在native函數的進口挪用PushLocalFram,在函數加入時挪用PopLocalFram,部分變量就會被釋放。

jobject f(JNIEnv *env, ...) 
{ 
  jobject result; 
  if ((*env)->PushLocalFrame(env, 10) < 0) { 
    /* frame not pushed, no PopLocalFrame needed */ 
    return NULL; 
  } 
  ... 
  result = ...; 
  if (...) { 
    /* remember to pop local frame before return */ 
    result = (*env)->PopLocalFrame(env, result); 
    return result; 
  } 
  ... 
  result = (*env)->PopLocalFrame(env, result); 
  /* normal return */ 
  return result; 
} 

PopLocalFram函數挪用掉敗時,能夠會招致不決義的行動,例如VM瓦解。

內存洩露成績
Java Heap 的內存洩露
Java 對象存儲在 JVM 過程空間中的 Java Heap 中,Java Heap 可以在 JVM 運轉進程中靜態變更。假如 Java 對象愈來愈多,占領 Java Heap 的空間也愈來愈年夜,JVM 會在運轉時擴大 Java Heap 的容量。假如 Java Heap 容量擴大到下限,而且在 GC 後依然沒有足夠空間分派新的 Java 對象,便會拋出 out of memory 異常,招致 JVM 過程瓦解。
Java Heap 中 out of memory 異常的湧現有兩種緣由——①法式過於宏大,導致過量 Java 對象的同時存在;②法式編寫的毛病招致 Java Heap 內存洩露。
多種緣由能夠招致 Java Heap 內存洩露。JNI 編程毛病也能夠招致 Java Heap 的內存洩露。
JVM 中 native memory 的內存洩露
從操作體系角度看,JVM 在運轉時和其它過程沒有實質差別。在體系級別上,它們具有異樣的調劑機制,異樣的內存分派方法,異樣的內存格式。
JVM 過程空間中,Java Heap 之外的內存空間稱為 JVM 的 native memory。過程的許多資本都是存儲在 JVM 的 native memory 中,例如載入的代碼映像,線程的客棧,線程的治理掌握塊,JVM 的靜態數據、全局數據等等。也包含 JNI 法式中 native code 分派到的資本。
在 JVM 運轉中,多半過程資本從 native memory 中靜態分派。當愈來愈多的資本在 native memory 平分配,占領愈來愈多 native memory 空間而且到達 native memory 下限時,JVM 會拋出異常,使 JVM 過程異常加入。而此時 Java Heap 常常還沒有到達下限。
多種緣由能夠招致 JVM 的 native memory 內存洩露。例如 JVM 在運轉中過量的線程被創立,而且在同時運轉。JVM 為線程分派的資本便可能耗盡 native memory 的容量。
JNI 編程毛病也能夠招致 native memory 的內存洩露。對這個話題的評論辯論是本文的重點。

JNI 編程完成了 native code 和 Java 法式的交互,是以 JNI 代碼編程既遵守 native code 編程說話的編程規矩,同時也遵照 JNI 編程的文檔標准。在內存治理方面,native code 編程說話自己的內存治理機制仍然要遵守,同時也要斟酌 JNI 編程的內存治理。
本章簡略歸納綜合 JNI 編程中不言而喻的內存洩露。從 native code 編程說話本身的內存治理,和 JNI 標准附加的內存治理兩方面停止論述。
Native Code 自己的內存洩露
JNI 編程起首是一門詳細的編程說話,或許 C 說話,或許 C++,或許匯編,或許其它 native 的編程說話。每門編程說話情況都完成了本身的內存治理機制。是以,JNI 法式開辟者要遵守 native 說話自己的內存治理機制,防止形成內存洩露。以 C 說話為例,當用 malloc() 在過程堆中靜態分派內存時,JNI 法式在應用完後,應該挪用 free() 將內存釋放。總之,一切在 native 說話編程中應該留意的內存洩露規矩,在 JNI 編程中仍然順應。
Native 說話自己引入的內存洩露會形成 native memory 的內存,嚴重情形下會形成 native memory 的 out of memory。
Global Reference 引入的內存洩露
JNI 編程還要同時遵守 JNI 的標准尺度,JVM 附加了 JNI 編程獨有的內存治理機制。
JNI 中的 Local Reference 只在 native method 履行時存在,當 native method 履行完後主動掉效。這類主動掉效,使得對 Local Reference 的應用絕對簡略,native method 履行完後,它們所援用的 Java 對象的 reference count 會響應減 1。不會形成 Java Heap 中 Java 對象的內存洩露。
而 Global Reference 對 Java 對象的援用一向有用,是以它們援用的 Java 對象會一向存在 Java Heap 中。法式員在應用 Global Reference 時,須要細心保護對 Global Reference 的應用。假如必定要應用 Global Reference,務必確保在不消的時刻刪除。就像在 C 說話中,挪用 malloc() 靜態分派一塊內存以後,挪用 free() 釋放一樣。不然,Global Reference 援用的 Java 對象將永久逗留在 Java Heap 中,形成 Java Heap 的內存洩露。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved