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