JNI全域性引用與JFrame.dispose()方法

鐵錨發表於2017-01-28

問題描述

用 jProfiler 分析 Java swing 程式中的記憶體洩漏問題時, 我發現記憶體中 JFrame 例項的數量一直在增加。

各個 frame 被開啟(opened),然後被關閉(closed)。

通過 jProfiler, 並檢視GC Root時, 只找到一項: ‘JNI Global reference’。

這是什麼意思? 為什麼他 hang 住了所有的 frame 例項?

回答1

請檢視《維基百科》中關於 Java本地介面 的介紹, 本質上它允許 Java程式 和系統庫之間進行通訊。

JNI全域性引用很容易造成記憶體洩漏, 因為它們不能被自動垃圾收集所清理, 程式設計師必須顯式地釋放它們. 如果你沒有編寫任何JNI程式碼, 那麼狠可能是使用的庫中存在記憶體洩漏。

修正: 請參考關於 local vs. global references 的更多資訊. 其中介紹了為什麼要使用全域性引用(以及如何進行釋放)。

回答2

JNI全域性引用(JNI global reference), 是從 “native” 程式碼指向堆記憶體中Java物件的引用. 其存在的目的是阻止垃圾收集器, 不要誤將 native 程式碼中仍在使用的物件給回收了, 假如這些Java物件沒有Java程式碼引用到他們的話。

一個 JFrame 例項就是一個視窗(java.awt.Window), 並關聯到一個本地的 Window 物件。如果某個特定 JFrame 例項的任務已經結束,那麼就應該呼叫 dispose() 方法來執行清理。

我不確定原生程式碼是否建立了全域性引用來指向 JFrame, 但應該是這樣沒錯。如果確實建立了全域性引用, 那就會阻止 JFrame 物件被GC回收. 如果程式中建立了很多 Window 物件(或其子類物件), 而又沒有呼叫 dispose(), 則他們永遠都不會被GC回收掉, 這就造成了隱形的記憶體洩露。

區域性引用和全域性引用簡介

JNI為所有傳遞給 native 方法的物件型引數建立了引用, 同時也對所有從JNI函式返回的物件建立引用。

這些引用會阻止Java物件被GC清理。為了確保Java物件最終被釋放, JNI預設建立的是區域性引用(local references). 當本機方法返回時, 其建立的區域性引用就會失效。當然, native 方法不應該將區域性引用存到其他地方, 妄圖在後續呼叫中進行重用。

例如,以下程式(FieldAccess.c 中的一種變體native方法) 錯誤地將Java類的 ID field 快取起來, 期待不必每次都通過欄位名稱和簽名去搜尋 ID field ,:

/* !!! 這是一段問題程式碼 */
static jclass cls = 0;
static jfieldID fid;

JNIEXPORT void JNICALL
Java_FieldAccess_accessFields(JNIEnv *env, jobject obj)
{
  ...
  if (cls == 0) {
    cls = (*env)->GetObjectClass(env, obj);
    if (cls == 0)
      ... /* error */
    fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
  }
  ... /* access the field using cls and fid */
}

這個程式是錯誤的,因為從 GetObjectClass 返回的區域性引用只在 native 方法返回前才有效。第二次進入 Java_FieldAccess_accessField 方法時, 將會引用到一個無效的 local reference。 這會引起錯誤的結果甚至導致 JVM 崩潰。

要解決這種問題, 可以建立全域性引用(global reference)。全域性引用將一直有效,直到顯式釋放:

/* 本段程式碼是OK的 */
static jclass cls = 0;
static jfieldID fid;

JNIEXPORT void JNICALL
Java_FieldAccess_accessFields(JNIEnv *env, jobject obj)
{
  ...
  if (cls == 0) {
    jclass cls1 = (*env)->GetObjectClass(env, obj);
    if (cls1 == 0)
      ... /* error */
    cls = (*env)->NewGlobalRef(env, cls1);
    if (cls == 0)
      ... /* error */      
    fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
  }
  ... /* access the field using cls and fid */
}

全域性引用一直存在,直到Java類被解除安裝之後。 因此保證了在下次用到Java類的ID欄位時其一直有效。 native 程式碼不再使用全域性引用時必須呼叫 DeleteGlobalRefs 函式; 否則,對應的Java物件(如 cls引用的Java類)永遠都不會被解除安裝。

在大多數情況下, native 程式設計師應該依靠VM來釋放所有的區域性引用. 在某些特殊情況下,native 程式碼也可以呼叫 DeleteLocalRef 函式來顯式地刪除區域性引用。這些情況包括:

引用了某個龐大的Java物件, 不想等當前 native 方法返回時才讓Java物件被GC回收。例如,在下面的程式中, GC 可以釋放lref所引用的Java物件, 而此時 lengthyComputation 方法還在執行中:

  lref = ...            /* a large Java object */
  ...                   /* last use of lref */
  (*env)->DeleteLocalRef(env, lref);

  lengthyComputation(); /* may take some time */

  return;               /* all local refs will now be freed */
}

也可能會在 native 方法中建立大量的區域性引用。這很可能會導致 JNI local reference table 溢位. 這時候刪除那些不用的區域性引用是挺有必要的。 例如下面的程式中, native 程式碼遍歷一個由 java字串組成的大陣列. 每次迭代之後,都可以釋放字串元素的區域性引用:

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

參考:

翻譯人員: 鐵錨 http://blog.csdn.net/renfufei

翻譯時間: 2017年01月28日


相關文章