通過使用Invocation API,使用C/C++開發的本地應用可以訪問Java虛擬機提供的特性。為了描述簡單,下面提到的VM指的都是Java虛擬機。
在本地應用裡,調用JNI_CreateJavaVM()方法可以完成初始化、加載VM,並返回指向新VM對象的一個指針。調用JNI_CreateJavaVM方法的線程,被稱為主線程。
JNIEnv對象並不是線程安全的,因此只能在當前線程使用。當需要跨線程使用JNIEnv對象時,需要通過調用AttachCurrentThread方法將當前線程與JVM進行關聯,並得到一個指向JNIEnv對象的指針。當AttachCurrentThread方法調用成功之後,當前本地線程即可被VM感知。由於本地線程並不由JVM創建,因而需要確保自身有足夠的棧空間來執行必要的代碼。
調用者可以在本地線程中調用DetachCurrentThread來解除關聯關系,以便於釋放資源,否則會導致資源洩漏;但在本地線程的調用棧內仍有Java方法時,調用DetachCurrentThread方法可能會失敗。
當VM使用完畢,就應當考慮停止VM並回收資源,通過調用JNI_DestroyJavaVM方法即可達到這一目的。在VM看來,用戶線程包括VM在執行Java字節碼時創建的Java線程,以及通過調用AttachCurrentThread方法進而與VM完成關聯的本地線程。用戶線程的代碼在執行時,可能會持有比如鎖、窗口之類的系統資源,為了簡化VM的實現,VM把釋放資源的操作留給程序員去做,VM要求調用JNI_DestroyJavaVM方法的當前線程必須是當前唯一存活的用戶線程,否則JNI_DestroyJavaVM方法調用後可能無法達到預期的效果。
本地動態庫被VM加載之後,對於VM內部所有的類加載器都是可見的。即VM內部由不同類加載器加載的兩個類可以關聯到相同的本地方法,這帶來兩個問題:
一個Java類可能會與由另外一個類加載觸發加載的本地庫建立關聯關系;本地方法無法區分當前的調用是來自VM內部由不同類加載器加載的哪個類,這破壞了由類加載器控制的類命名空間,從而可能引入類型安全相關的問題;
為了解決上述的兩個問題,引入了新的解決方法,即某個類加載器自己管理當前加載的本地庫的集合,並且相同的本地庫只能被一個類加載器加載。應用的代碼違反這兩點約束將導致UnsatisfiedLinkError的出現。
新方法的優點有:
基於類加載器實現的命名空間管理在本地庫的使用方面得到了保留,本地庫可以無需考慮來自不同類加載器的類的調用;當加載某個本地庫的類加載器被GC掉之後,本地庫也可以自動被釋放掉資源。
為了便於實現上述特性,VM暴露了方法JNI_OnLoad。在VM加載本地庫時,VM會自動在本地庫文件中查找這個方法,如果方法存在則通過這個方法來獲取本地庫使用的JNI版本號,這樣VM可以決定本地庫使用VM特性的請求是否合理。如果JNI_OnLoad方法沒有實現,VM認為本地庫基於JNI_VERSION_1_1相關的特性實現。如果VM無法識別JNI_OnLoad方法的返回值,VM會忽略本地庫的加載請求,並清理現場。
當加載本地庫的類加載器被GC之後,VM會主動調用本地庫導出的JNI_OnUnLoad方法,如果本地庫沒定義這個方法的話,這個步驟將自動忽略。一般而言,可以在JNI_OnUnLoad方法內部做一些清理操作。由於JNI_OnUnLoad方法被VM回調的時機不確定,因而要避免在這個方法內部調用Java語言的方法以及VM提供的特性。
這一章節提到的API均由VM提供。方法的返回值為JNI_OK 時表示調用成功,非JNI_OK 表示調用失敗。
如下是常用的幾個結構定義。
typedef struct JavaVMInitArgs { jint version; jint nOptions; JavaVMOption *options; jboolean ignoreUnrecognized; } JavaVMInitArgs; typedef struct JavaVMOption { char *optionString; /* the option as a string in the default platform encoding */ void *extraInfo; } JavaVMOption; typedef struct JavaVMAttachArgs { jint version; char *name; /* the name of the thread as a modified UTF-8 string, or NULL */ jobject group; /* global ref of a ThreadGroup object, or NULL */ } JavaVMAttachArgs;
通過調用本方法,可以卸載已創建的VM對象,並回收相關的資源。本方法是線程安全的,可以在任意線程使用。本方法在使用時會阻塞當前線程嗎?假如調用線程沒有與VM對象建立了關聯關系,則直接建立關聯關系,然後等待其它用戶線程退出;假如已建立了關聯關系,則直接等待其它用戶線程退出。
Unloading of the VM is not supported.這語沒有看明白什麼意思。
調用本方法時,第一個參數為指向VM對象的指針,第二個參數為JNIEnv類型對象的指針,第三個參數為JavaVMAttachArgs類型對象的指針,但沒有實際用途,應當為設置為NULL(不過原文檔對這個參數的介紹稍有點混亂,需要實際驗證一下)。
用法和參數與AttachCurrentThread方法類似,區別在於VM內部創建的java.lang.Thread對象將會是一個daemon。如果當前本地線程已經和VM建立關聯,則多次調用AttachCurrentThread或者AttachCurrentThreadAsDaemon並不會修改java.lang.Thread對象的daemon屬性。
當前線程與VM解除關聯,本地線程持有的鎖對象將全部釋放。等待當前線程的Java線程將得到通知。主線程通過調用本方法,可以和VM解除關聯。
通過調用本方法,可以獲取到當前線程的JNIEnv對象。如果當前線程與VM沒有建立關聯,則*env被設置為NULL,同時返回JNI_EDETACHED;如果傳入的VM特性版本號不被支持,則*env被設置為NULL,同時返回JNI_EVERSION;如果調用成功,則*env被設置為正確的JNIEnv對象指針,同時返回JNI_OK。