開發與維運

JNI in Java

一、什麼是JNI 

(一)什麼是JNI (Java Native Interface) 

JNI全稱是Java Native Interface,顧名思義是Java和Native間的通信橋樑,如下圖所示,圖的上方是Java世界,下面是Native世界,中間是JNI通信,左邊箭頭從上往下是Java調用Native的方法,右邊是Native調用Java,彼此可以互通。 

image.png 

這種方式帶來的好處Java調用Native,可以去調用非Java實現的庫,擴充Java的使用場景比如調用Tensorflow反之Native調用Java,可以在別的語言裡面調用Java,比如java launcher可以命令啟動Java程序 

(二)為什麼要學習JNI 

掌握Java和Native之間的互相調用,大大豐富java的使用場景瞭解原理,對於學習JVM/故障定位更加得心應手 

經典例子,如下圖所示,在主函數裡面用Selector.open創建一個selectselect方法,這是Java裡面通過NIO取允許網絡的方法。 

image.png 

public static void main(String[] args) throws Exception { 

        java.nio.channels.Selector.open().select(); 

    } 

這個方法會阻塞其當前線程通過java.lang呈現狀態是RUNNABLE看到RUNNABLE總覺得消耗CPU、NIO的BUG, 其實是一個經典謬誤,實際上線程是禁止的 

 

二、JNI實踐和思考 

實戰: 從native調用Java 

首先#include <jni.h> 這個頭文件定義了各種Java和Native交互的數據結構以及定義在主函數裡面,首先聲明一個JVM的指針,然後一個JNIEnv *env的指針,JVM表示的Java虛擬實,我通過實例消耗資源進行各種操作。 

env其實對應的是一個線程,然後創建JavaVMInitArgs結構體結構體裡面要填充Java參數,JavaVMOption表示因為這裡不需要參數,場景比較簡單,所以用options[0]把 options傳入 vm_args.options結構體,最後調用JNI_CreateJavaVM創建 Java虛擬器,如果返回的是JNI_OK,說明這次調用成功 

有了JNI指針表示實例以後,就可以用標準方法使用JNI在這裡調用一個Java方法,比如Java數據結構先通過EMCFindClass找到SelectorProvider類,中間有個printf變量叫lock,先通過 GetStaticField獲取 field再通過GetStaticObjectField從 cls對象上獲取fid就是 lock對象,然後把它打印出來,最後jvm->DestroyJavaVM。詳情操作如下圖所示: 

image.png 

還有一個比較經典的例子Java Launcher java –jar spring-application執行程序的時候,在後臺默默的創建了一個jvmJava參數作為 arguments傳進去,調用Java入口方法通過JNI實現 

image.png 

平時說,開發jvm其實就是開發jvm的動態庫, libjvm.so基本上本身是作為os提供出去,好處是非常靈活,可以作為獨立應用使用,也可以在別的像cer這樣的語言調用使Java調用NativeNative調用Java靈活。 

 

JNI實戰Java調用C 

Java調用C使用JNI最常見的方式首先定一個類叫HelloJNI裡面有System.loadLibrary("hello"); 系統會自動去找到library libhello.so這個類裡面定義方法叫sayHello,加了C以後調用它,但這是調不通的,因為並沒有提供真正的Native實現實現要通過一個頭文件去告訴這個方法的簽名,這裡實現Java文件,然後通過jni.h生成頭文件,這個是自動生成的。 

簽名是 Java,然後是Java_HelloJNI_sayHello(JNIEnv *, jobject)規範,類名加上方法名,參數第一個是環境第二個是jobject,無參數,但是 Java的方法默認是有一個this指針作為第一個參數,最後編寫它,實現HelloJNI.c根據這個聲明定義實現,然後裡面只是printf一下,把 HelloJNI.c定義成libhelloHello.so這個程序就可以運行起來了。詳情如下圖所示: 

image.png 

在Java應用裡面,可以調通過JNI調用各種庫,調用到native以後,因為任何語言跟native都互相交互,大大豐富了Java使用場景。 

image.png 

 

思考Java和Native的數據是怎麼傳遞的 

在執行Java方法時,用的是java heap,假設暫時向下增長,需要調用 c函數的時候,它需要去壓站,把 object壓站JNIEnv壓站cstack壓站,進入seat stack然後 object本質上是指向handle的指針,handle指向戰上真正的OOP,使用二級指針結構,稍微有點複雜。詳情如下圖所示: 

image.png 

 

思考: 回到問題,為什麼select()的線程狀態是RUNNABLE 

JNI只是提供一種機制,讓Java程序可以進入Native狀態,Native狀態基本上沒有辦法管理。這段Native代碼在做一種非常複雜的數學運算,肯定是RUNNABLE狀態,也可以調用系統形象去阻塞,但這個阻塞基本上不知情,所以會一直顯示為RUNNABLE,除非通過JNI的特殊接口改變現實狀態,到其他狀態才會顯示為其他狀態,所以這裡顯示為RUNNABLE正常不用擔心RUNNABLE狀態消耗很多CPU問題。 

image.png 

 

三、JNI與safepoint 

首先有這樣兩個問題: 

1JNI是否會影響GC進行 

2、GC時JNI修改Java Heap怎麼保證一致 

看到第二個問題的時候,已經回答第一個問題,假如GC是不能運行JNI,那也就沒有一致性問題,所以在GC可以執行JNI 

 

(一)JNI與Safepoint的協作 

首先要知道Java的信任狀態,Java最主要信任狀態是Thread in Java狀態,這狀態裡面在執行一個解釋器或者已經編譯的方法,純Java執行這時候如果發生Safepoint會通過Interpreter機制把這個線程直接掛起,暫停下來,然後去Safepoint裡面進行GC的各種操作。 

在Java裡面,調用JNI進入Native會切換到Thread in native狀態,這裡執行Native函數,在執行的時候跟GC可以並行執行,因為理論上要麼執行,要麼通過JNIJNI交互,所有的跟JNI相關的數據結構都可以被管理。然後Native還可以去切換到JVM狀態,這是非常關鍵的狀態,這個狀態不能發生GC不用關心 

JNI與Safepoint交互,假如JNI執行時發生Safepoint能並行JNI執行的時候返回Java,這時候會被阻塞需要檢查狀態,卡在Safepoint狀態,直到Safepoint結束,繼續回到Java。 

image.png 

(二)JNI與GC 

透過幾個JNI管中窺豹瞭解這個機制 

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);  

void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carrayjint mode); 

這個函數叫做GetPrimitiveArrayCriticalCritical作用是把一段內存返回給用戶,用戶可以直接編輯裡面的數據這時如果發生GC被移動編輯肯定會導致 heap亂掉,Critical這段時間裡鎖住heap沒法發生GC。假如 critical狀態發生期間,基本上不會影響GC會等待,直ReleasePrimitiveArrayCritical發出這是比較巧妙的互相協作。 

下圖所示的二級指針模型還是前面Java調到Native,參數通過jobjecthandle保存使用jobject指向handlehandle指向oop 

image.png 

java heap時候假如OOP對象被移動handle,同時會更新 handle裡面的地址所以只要C程序都是通過JNI訪問對象,每次對象被移動它都可以被感知,不會出現數據佈局之後突然情況 

“GC: handle_are->oops_do(f)” 

指有區域專門存放handle,裡面所有handle在GC裡,都會進行一次指針修正,保證數據一致性。 

四、JNI與Intrinsic 

(一)高級主題: intrinsic 

如下圖所示,非常常見JNA“currentThread為例子,說明Intrinsic機制。Intrinsic在看到currentThread的時候不會去JNI,而是通過形成更高效的版本。 

這裡inline_native_currentThread的時候最終會調用generate_curent_thread工具然後看裡面的實現核心部分,創建ThreadLocalNode(),代表當前JavaThread結構的指針,再通過JavaThread結構裡的threadObj_offset()拿到它,通常是一個偏移量,拿到Object以後作為返回值返回這裡是一段AI,真正生成代碼被翻譯成非常簡約的幾條指令,直接返回。所以currentThread變得非常高效,這就是Intrinsic機制,主要為性能而生 

image.png 

image.png 

 

(二)Intrinsic性能分析 

對比一下IntrinsicIntrinsic性能,如下圖所示,是jmh寫的Benchmark,可以規避掉一些具體的預熱不夠導致性能測試不準問題,用它進行測試,也是官方推薦的版本 

Intrinsic版本,下面測試叫“jni”,主要區別就是Intrinsic後面接了一個叫isAlive的調用。isAlive本身狀態調用看起來非常輕量,但因為他沒有做Intrinsic,所以最終會走JNI 

image.png 

 

下圖所示,對比普通Intrinsic加上JNIIntrinsic性能,普通 Intrinsic的性能大概是3億次每秒加上JNI的Intrinsic版本的性能是2000萬次每秒,差了十幾倍,差距很大 

image.png 

 

進一步性能問題,最重要的是performingperforming手段performpublic第二段JNI版本,前面兩個熱點方法都是ThreadStateTransition狀態轉換。前面說到,假如JNI回到 Java時候做GC肯定要停下來,所以這有個內存同步比較好資源,要等的時間比較長,所以這兩個函數是最熱的。 

image.png 

下面是JVM_IsThreadAlive實現。後面是HandleMark::pop_and_restore在調JNI時需要把oop包裝handle,JNI退出時需要消費handle restore有開銷。再後面java_lang_Thread::is_alive佔比4.77% 非常小。 

由此可以看出Intrinsic提供性能非常好的機制,直接調用JNI性能可能差一點,但也可以接受 

 

(三)案例分析: RocketMQ Intrinsic導致應用卡頓 

RocketMQ 是阿里巴巴開源的MQ產品,使用非常廣泛裡面有個函數叫warmMappedFile指的是RocketMQ是通過warmMapped機制內存映射磁盤去做IO,在申請完一塊磁盤映射的內存以後,會去做預熱。 

這裡有for循環for (int i = 0, j = 0; i < this.fileSize”,每隔一個PACG_SIZEbyteBuffer.put(i, (byte) 0);,這樣的話操作系統就會發生缺,把內存真正分配出來,而不只是EMV數據結構。分配出來以後,等到程序真正使用這塊內存的時候,就是純內存IO,不太會觸發這種缺頁了,可以變得更快,目的是減少程序卡頓。 

image.png 

但是後面了if這一段,可以想到剛開始這個循環有問題,因為 byteBuffer.putIntrinsic,最底層是Intrinsic,方法返回的時候沒有方法調用。JVM在方法返回以及循環末尾檢查是否有Safepoints來看是否要進入GC但是因為這是一個Intrinsic,所以沒有到檢查點,同樣這是一個CountedLoop,也沒法去進入檢查點因為JVM有個機制,如果這是一個 int作為indexCounted次數的話,為了性能是不會去檢查,因為它認為這是有限次的循環,所以不用檢查次數。 

這種機制循環裡面非常簡單,中間有可能因為操作系統原因帶來頓,導致循環,基本上沒法進入GC,因為線程有進入Safepoints,整個界面都沒法進入GC, 夯住很長時間,當時大家覺得很不可思議,但是通過一個很簡單方法修好了,就是每隔1000字循環的時候,去調一個Thread.sleep(0) 

剛剛提到,“byteBuffer.put沒法出發,Thread.sleep是個JNI返回的時候會檢查Safepoints,所以就可以讓這個程序能夠進入到Safepoints這個代碼就不會影響JVM進入到GC了,代碼目前還可以從開源軟件上看到。 

“-XX:+UseCountedLoopSafepoints 

解決這個問題,還有另一種方式通過一個選項叫“-XX:+UseCountedLoopSafepoints,可以JVM自動在CountedLoop結尾檢查這Safepoints當然這帶來的副作用CountedLoop末尾都會檢查Safepoints,這樣就會影響整體性能。 

Leave a Reply

Your email address will not be published. Required fields are marked *