【譯】JVM Anatomy Park #9: JNI 臨界區(qū) 與 GC 鎖

原文地址:JVM Anatomy Park #9: JNI Critical and GC Locker

問題

JNI Get*Critical 如何與 GC 協(xié)同钦奋?GC Locker 是什么迷殿?

理論

如果你熟悉 JNI,那么你會知道有兩組方法可以獲取數(shù)組內(nèi)容滤奈。一組是 Get<PrimitiveType>Array* 方法,另一組是這些

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

這兩個方法的語義與 Get/Release*ArrayElements 方法類似闯睹。如果可能罩扇,VM 將返回原始數(shù)組的指針;否則返回一個副本蜂怎。然而穆刻,在使用上存在很多限制。

—— 《JNI 指南》 第四章:JNI 方法

這樣做的好處很明顯:與其給你一個 Java 數(shù)組的副本杠步,VM 可以直接給你返回一個指針氢伟,這樣就能提高性能。當(dāng)然這樣做也有很多坑幽歼,下面將一一羅列:

調(diào)用 GetPrimitiveArrayCritical 方法之后朵锣,本地代碼在調(diào)用 ReleasePrimitiveArrayCritical 之前不能執(zhí)行太長時間。我們需要將這兩次調(diào)用之間的代碼當(dāng)做“臨界區(qū)”看待甸私。在臨界區(qū)中诚些,本地代碼不能調(diào)用其它的 JNI 方法,也不能執(zhí)行引起當(dāng)前線程阻塞等待其它線程的系統(tǒng)調(diào)用。(例如诬烹,當(dāng)前線程不能讀取其它線程寫的流)

這些限制使得本地代碼更可能獲取非拷貝的數(shù)組砸烦,即使 VM 不支持釘住。例如绞吁,當(dāng)本地代碼持有 GetPrimitiveArrayCritical 返回的指針時幢痘,VM 可能會暫時關(guān)閉垃圾收集。

—— 《JNI 指南》 第四章:JNI 方法

這一段讀起來的意思好像是家破,在臨界區(qū)執(zhí)行的時候 VM 將會停止 GC颜说。

實際上對于 VM 來說唯一的強不變式是維護在“臨界區(qū)”持有的對象不被移動。有很多不同的實現(xiàn)策略可以嘗試:

  1. 當(dāng)持有臨界區(qū)對象的時候完全關(guān)閉 GC汰聋。這就是最簡單的復(fù)制策略门粪,因為這將不會影響接下來的 GC。缺點是你不得不無限期的阻塞 GC(只能寄希望于用戶足夠快的“釋放”)马僻,這將會造成很多問題庄拇。
  2. 釘住對象,在收集的時候忽略它韭邓。如果收集器期望分配連續(xù)的空間措近,或者期望處理整個堆子空間,那么就比較難實現(xiàn)了女淑。例如瞭郑,如果你將對象分配在新生代,那么就不能簡單的“忽略”收集了鸭你。你也不能移動對象屈张,因為這就打破了不變式。
  3. 釘住包含對象的子空間袱巨。如果 GC 的粒度是整代阁谆,那么也很難實現(xiàn)。但是如果你的堆是分塊的愉老,那么你可以釘住單個塊场绿,讓 GC 忽略這個塊,這樣就能實現(xiàn)不變式了嫉入。

我們曾經(jīng)看到有些人依賴 JNI 臨界區(qū)暫時關(guān)閉 GC焰盗,但是這僅僅對第一種策略有效,實際上并不是每個收集器都采用這種最簡單的策略咒林。

我們可以通過代碼驗證么熬拒?

實驗

像往常一樣,我們可以這樣構(gòu)建測試用例垫竞,在 JNI 臨界區(qū)獲取 int[] 數(shù)組澎粟,然后故意忽略釋放數(shù)組的建議。相反,我們在獲取和釋放之間分配并持有大量對象:

public class CriticalGC {

  static final int ITERS = Integer.getInteger("iters", 100);
  static final int ARR_SIZE = Integer.getInteger("arrSize", 10_000);
  static final int WINDOW = Integer.getInteger("window", 10_000_000);

  static native void acquire(int[] arr);
  static native void release(int[] arr);

  static final Object[] window = new Object[WINDOW];

  public static void main(String... args) throws Throwable {
    System.loadLibrary("CriticalGC");

    int[] arr = new int[ARR_SIZE];

    for (int i = 0; i < ITERS; i++) {
      acquire(arr);
      System.out.println("Acquired");
      try {
        for (int c = 0; c < WINDOW; c++) {
          window[c] = new Object();
        }
      } catch (Throwable t) {
        // omit
      } finally {
        System.out.println("Releasing");
        release(arr);
      }
    }
  }
}

本地代碼部分:

#include <jni.h>
#include <CriticalGC.h>

static jbyte* sink;

JNIEXPORT void JNICALL Java_CriticalGC_acquire
(JNIEnv* env, jclass klass, jintArray arr) {
   sink = (*env)->GetPrimitiveArrayCritical(env, arr, 0);
}

JNIEXPORT void JNICALL Java_CriticalGC_release
(JNIEnv* env, jclass klass, jintArray arr) {
   (*env)->ReleasePrimitiveArrayCritical(env, arr, sink, 0);
}

我們需要生成合適的頭文件捌议,將本地代碼編譯鏈接成庫文件哼拔,然后確保 JVM 可以加載庫文件引有。所有的文件都打包在這里瓣颅。

Parallel/CMS

首先觀察 Parallel 收集器的行為:

$ make run-parallel
java -Djava.library.path=. -Xms4g -Xmx4g -verbose:gc -XX:+UseParallelGC CriticalGC
[0.745s][info][gc] Using Parallel
...
[29.098s][info][gc] GC(13) Pause Young (GCLocker Initiated GC) 1860M->1405M(3381M) 1651.290ms
Acquired
Releasing
[30.771s][info][gc] GC(14) Pause Young (GCLocker Initiated GC) 1863M->1408M(3381M) 1589.162ms
Acquired
Releasing
[32.567s][info][gc] GC(15) Pause Young (GCLocker Initiated GC) 1866M->1411M(3381M) 1710.092ms
Acquired
Releasing
...
1119.29user 3.71system 2:45.07elapsed 680%CPU (0avgtext+0avgdata 4782396maxresident)k
0inputs+224outputs (0major+1481912minor)pagefaults 0swaps

注意在“Acquired”與“Released”之間沒有發(fā)生 GC,所以實現(xiàn)的細節(jié)就很容易猜到了譬正。確鑿的證據(jù)是“GCLocker Initiated GC”宫补。GCLocker 是阻止 JNI 臨界區(qū)發(fā)生 GC 的≡遥看一下 OpenJDK 代碼中的相關(guān)片段

JNI_ENTRY(void*, jni_GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy))
  JNIWrapper("GetPrimitiveArrayCritical");
  GCLocker::lock_critical(thread);   // <--- acquire GCLocker!
  if (isCopy != NULL) {
    *isCopy = JNI_FALSE;
  }
  oop a = JNIHandles::resolve_non_null(array);
  ...
  void* ret = arrayOop(a)->base(type);
  return ret;
JNI_END

JNI_ENTRY(void, jni_ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode))
  JNIWrapper("ReleasePrimitiveArrayCritical");
  ...
  // The array, carray and mode arguments are ignored
  GCLocker::unlock_critical(thread); // <--- release GCLocker!
  ...
JNI_END

當(dāng)嘗試執(zhí)行 GC 的時候粉怕,JVM 將會檢查這個鎖是否被持有。如果某個線程持有鎖抒巢,那么就不能繼續(xù)執(zhí)行 GC贫贝,至少在 Parallel、CMS 和 G1 中是這樣蛉谜。當(dāng)下一個臨界區(qū) JNI 操作結(jié)束時“釋放”了鎖稚晚,VM 將會檢查是否有 GCLocker 阻塞的 GC,如果有那么就觸發(fā) GC型诚。這就產(chǎn)生了“GCLocker Initiated GC”客燕。

G1

當(dāng)然,因為我們正在玩火 —— 在 JNI 臨界區(qū)做奇怪的事情 —— 所以隨時可能爆炸狰贯。再觀察一下 G1 收集器的行為:

$ make run-g1
java -Djava.library.path=. -Xms4g -Xmx4g -verbose:gc -XX:+UseG1GC CriticalGC
[0.012s][info][gc] Using G1
<HANGS>

哎喲也搓!程序中止了。jstack 顯示處于 RUNNABLE 狀態(tài)涵紊,但是正在等待某個奇怪的條件:

"main" #1 prio=5 os_prio=0 tid=0x00007fdeb4013800 nid=0x4fd9 waiting on condition [0x00007fdebd5e0000]
   java.lang.Thread.State: RUNNABLE
  at CriticalGC.main(CriticalGC.java:22)

最簡單的查找線索的方法是執(zhí)行 “fastdebug” 構(gòu)建傍妒,那將會停止在這個有趣的斷言上:

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (/home/shade/trunks/jdk9-dev/hotspot/src/share/vm/gc/shared/gcLocker.cpp:96), pid=17842, tid=17843
#  assert(!JavaThread::current()->in_critical()) failed: Would deadlock
#
Native frames: (J=compiled Java code, A=aot compiled Java code, j=interpreted, Vv=VM code, C=native code)
V  [libjvm.so+0x15b5934]  VMError::report_and_die(...)+0x4c4
V  [libjvm.so+0x15b644f]  VMError::report_and_die(...)+0x2f
V  [libjvm.so+0xa2d262]  report_vm_error(...)+0x112
V  [libjvm.so+0xc51ac5]  GCLocker::stall_until_clear()+0xa5
V  [libjvm.so+0xb8b6ee]  G1CollectedHeap::attempt_allocation_slow(...)+0x92e
V  [libjvm.so+0xba423d]  G1CollectedHeap::attempt_allocation(...)+0x27d
V  [libjvm.so+0xb93cef]  G1CollectedHeap::allocate_new_tlab(...)+0x6f
V  [libjvm.so+0x94bdba]  CollectedHeap::allocate_from_tlab_slow(...)+0x1fa
V  [libjvm.so+0xd47cd7]  InstanceKlass::allocate_instance(Thread*)+0xc77
V  [libjvm.so+0x13cfef0]  OptoRuntime::new_instance_C(Klass*, JavaThread*)+0x830
v  ~RuntimeStub::_new_instance_Java
J 87% c2 CriticalGC.main([Ljava/lang/String;)V (82 bytes) ...
v  ~StubRoutines::call_stub
V  [libjvm.so+0xd99938]  JavaCalls::call_helper(...)+0x858
V  [libjvm.so+0xdbe7ab]  jni_invoke_static(...) ...
V  [libjvm.so+0xdde621]  jni_CallStaticVoidMethod+0x241
C  [libjli.so+0x463c]  JavaMain+0xa8c
C  [libpthread.so.0+0x76ba]  start_thread+0xca

仔細觀察調(diào)用鏈,我們可以重建發(fā)生的事情:嘗試分配新的對象摸柄,因為沒有 TLABs 滿足分配颤练,所以嘗試獲取新的 TLAB。然后發(fā)現(xiàn)沒有可用的 TLABs塘幅,嘗試分配昔案,失敗,發(fā)現(xiàn)需要等待 GCLocker 才能開始 GC电媳。進入 stall_until_clear 方法等待鎖踏揣。。匾乓。但是因為線程一直持有 GCLocker捞稿,這里的等待將會導(dǎo)致死鎖。爆炸

這是符合規(guī)范的娱局,因為這個測試用例嘗試在獲取釋放代碼塊中間分配對象彰亥。離開 JNI 方法而不調(diào)用 release 是錯誤的。在沒有離開 JNI 方法之前衰齐,不調(diào)用 JNI 是不能進行分配的任斋,而這違反了“不可調(diào)用 JNI 方法”的準(zhǔn)則。

你可以調(diào)整測試用例以避免這樣的問題耻涛,但是你會發(fā)現(xiàn) GCLocker 將會延遲收集废酷,這意味著僅剩很少空間的時候才會開始 GC,而這將會導(dǎo)致 Full GC抹缕。哎喲。

Shenandoah

就像理論描述的那樣卓研,分塊的收集器可以釘住持有對象的特定內(nèi)存塊,讓特定的內(nèi)存塊在 JNI 臨界區(qū)釋放前避免收集奏赘。Shenandoah 當(dāng)前就是這樣實現(xiàn)的。

$ make run-shenandoah
java -Djava.library.path=. -Xms4g -Xmx4g -verbose:gc -XX:+UseShenandoahGC CriticalGC
...
Releasing
Acquired
[3.325s][info][gc] GC(6) Pause Init Mark 0.287ms
[3.502s][info][gc] GC(6) Concurrent marking 3607M->3879M(4096M) 176.534ms
[3.503s][info][gc] GC(6) Pause Final Mark 3879M->1089M(4096M) 0.546ms
[3.503s][info][gc] GC(6) Concurrent evacuation  1089M->1095M(4096M) 0.390ms
[3.504s][info][gc] GC(6) Concurrent reset bitmaps 0.715ms
Releasing
Acquired
....
41.79user 0.86system 0:12.37elapsed 344%CPU (0avgtext+0avgdata 4314256maxresident)k
0inputs+1024outputs (0major+1085785minor)pagefaults 0swaps

注意志珍,在 JNI 臨界區(qū)持有期間值朋,CC 周期從開始到結(jié)束唤冈。Shenandoah 僅僅釘住持有數(shù)組的內(nèi)存塊靠娱,而其他的內(nèi)存塊正常進行收集。當(dāng) JNI 臨界區(qū)持有的對象在被收集的內(nèi)存塊中時也可以執(zhí)行 GC喂击,首先排除對應(yīng)的內(nèi)存塊,然后釘住這個塊(也就是將它排除出收集集合)淤翔。這就能實現(xiàn)不用 GCLocker 的 JNI 臨界區(qū)翰绊,因此也沒有 GC 延遲。

觀察

處理 JNI 臨界區(qū)需要 VM 的幫助旁壮,或者關(guān)閉 GC,或者采用 GCLocker 類似的機制抡谐,或者釘住包含對象的子空間,或者僅僅釘住對象麦撵。不同的 GCs 采用不同的策略處理 JNI 臨界區(qū)溃肪,某個收集器的副作用 —— 比如延遲 GC 周期 —— 可能并不會出現(xiàn)在另一個收集器中音五。

請注意規(guī)范中:在臨界區(qū)中,本地代碼不能調(diào)用其它 JNI 方法躺涝,這僅僅是最低的要求。上述測試表明莉撇,在規(guī)范允許的范圍內(nèi)呢蛤,實現(xiàn)的質(zhì)量決定了打破規(guī)范時的嚴(yán)重程度。某些 GC 更寬松银室,而某些會更嚴(yán)格励翼。如果你想要保證可移植性蜈敢,那么請遵循規(guī)范,而不是實現(xiàn)細節(jié)抓狭。

如果你依賴實現(xiàn)細節(jié)(這一個壞主意)造烁,使用 JNI 遇到了這些問題,那么需要理解收集器的處理策略惭蟋,并且選擇合適的 GC。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末煤伟,一起剝皮案震驚了整個濱河市木缝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌我碟,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卿叽,死亡現(xiàn)場離奇詭異桥胞,居然都是意外死亡,警方通過查閱死者的電腦和手機考婴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缎罢,“玉大人考杉,你說我怎么就攤上這事策精〕缣模” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵询刹,是天一觀的道長萎坷。 經(jīng)常有香客問我,道長哆档,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任澳淑,我火速辦了婚禮斟叼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘朗涩。我一直安慰自己,他們只是感情好兄一,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布识腿。 她就那樣靜靜地躺著,像睡著了一般渡讼。 火紅的嫁衣襯著肌膚如雪耳璧。 梳的紋絲不亂的頭發(fā)上展箱,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天混驰,我揣著相機與錄音攀隔,去河邊找鬼栖榨。 笑死,一個胖子當(dāng)著我的面吹牛满粗,可吹牛的內(nèi)容都是我干的居夹。 我是一名探鬼主播败潦,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼檬洞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起添怔,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤广料,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后艾杏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡畅铭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年勃蜘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炉擅。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谍失,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仿便,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布嗽仪,位于F島的核電站柒莉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏兢孝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一雳殊、第九天 我趴在偏房一處隱蔽的房頂上張望窗轩。 院中可真熱鬧,春花似錦痢艺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至友驮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卸留,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工旨指, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谆构。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像呵晨,于是被迫代替她去往敵國和親熬尺。 傳聞我的和親對象是個殘疾皇子摸屠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,292評論 0 10
  • 我走在大街上 感覺天空很靜 很靜
    看見_08d5閱讀 116評論 0 1
  • 許多年過去,仍舊改不掉愛脫鞋的臭毛病揭措。 彼時夏天很熱,教室很悶绊含,人也總是很燥。每天的晨跑和課間操壓榨了所有涼鞋的風(fēng)...
    The_outs閱讀 364評論 0 0
  • 無意中看到一本書的文案不翩,完美男人麻裳,于是我果斷的把書放在我的書架。 以前的我認為完美的男人應(yīng)該是英俊津坑、有...
    熊貓續(xù)夢閱讀 407評論 0 0
  • 文/憂喜 著名詩人李商隱的“此情可待成追憶昙啄?只是當(dāng)時已惘然”一詞,流傳千古梳凛,吟唱古今。確實淹接,有些事過去了就回不到原...
    憂喜閱讀 289評論 0 1