標籤:jni invocation api
本文是對連結http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html的學習筆記,限於英文水平和對JNI的理解,可能存在錯誤。
簡介
通過使用Invocation API,使用C/C++開發的本地應用可以訪問Java虛擬機器提供的特性。為了描述簡單,下面提到的VM指的都是Java虛擬機器。
建立VM
在本地應用裡,調用JNI_CreateJavaVM()方法可以完成初始化、載入VM,並返回指向新VM對象的一個指標。調用JNI_CreateJavaVM方法的線程,被稱為主線程。
線程與VM的關聯操作
JNIEnv對象並不是安全執行緒的,因此只能在當前線程使用。當需要跨線程使用JNIEnv對象時,需要通過調用AttachCurrentThread方法將當前線程與JVM進行關聯,並得到一個指向JNIEnv對象的指標。當AttachCurrentThread方法調用成功之後,當前本地線程即可被VM感知。由於本地線程並不由JVM建立,因而需要確保自身有足夠的棧空間來執行必要的代碼。
調用者可以在本地線程中調用DetachCurrentThread來解除關聯關係,以便於釋放資源,否則會導致資源泄漏;但在本地線程的調用棧內仍有Java方法時,調用DetachCurrentThread方法可能會失敗。
卸載VM
當VM使用完畢,就應當考慮停止VM並回收資源,通過調用JNI_DestroyJavaVM方法即可達到這一目的。在VM看來,使用者線程包括VM在執行Java位元組碼時建立的Java線程,以及通過調用AttachCurrentThread方法進而與VM完成關聯的本地線程。使用者線程的代碼在執行時,可能會持有比如鎖、視窗之類的系統資源,為了簡化VM的實現,VM把釋放資源的操作留給程式員去做,VM要求調用JNI_DestroyJavaVM方法的當前線程必須是當前唯一存活的使用者線程,否則JNI_DestroyJavaVM方法調用後可能無法達到預期的效果。
動態庫和版本管理
本地動態庫被VM載入之後,對於VM內部所有的類載入器都是可見的。即VM內部由不同類載入器載入的兩個類可以關聯到相同的本地方法,這帶來兩個問題:
- 一個Java類可能會與由另外一個類載入觸發載入的本地庫建立關聯關係;
- 本地方法無法區分當前的調用是來自VM內部由不同類載入器載入的哪個類,這破壞了由類載入器控制的類命名空間,從而可能引入型別安全相關的問題;
為瞭解決上述的兩個問題,引入了新的解決方案,即某個類載入器自己管理當前載入的本地庫的集合,並且相同的本地庫只能被一個類載入器載入。應用的代碼違反這兩點約束將導致UnsatisfiedLinkError的出現。
新方法的優點有:
- 基於類載入器實現的命名空間管理在本地庫的使用方面得到了保留,本地庫可以無需考慮來自不同類載入器的類的調用;
- 當載入某個本地庫的類載入器被GC掉之後,本地庫也可以自動被釋放掉資源。
JNI_OnLoad
為了便於實現上述特性,VM暴露了方法JNI_OnLoad。在VM載入本地庫時,VM會自動在本地庫檔案中尋找這個方法,如果方法存在則通過這個方法來擷取本地庫使用的JNI版本號碼,這樣VM可以決定本地庫使用VM特性的請求是否合理。如果JNI_OnLoad方法沒有實現,VM認為本地庫基於JNI_VERSION_1_1相關的特性實現。如果VM無法識別JNI_OnLoad方法的返回值,VM會忽略本地庫的載入請求,並清理現場。
JNI_OnUnLoad
當載入本地庫的類載入器被GC之後,VM會主動調用本地庫匯出的JNI_OnUnLoad方法,如果本地庫沒定義這個方法的話,這個步驟將自動忽略。一般而言,可以在JNI_OnUnLoad方法內部做一些清理操作。由於JNI_OnUnLoad方法被VM回調的時機不確定,因而要避免在這個方法內部調用Java語言的方法以及VM提供的特性。
Invocation API簡介
這一章節提到的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;
JNI_GetDefaultJavaVMInitArgs簽名為jint JNI_GetDefaultJavaVMInitArgs(void *vm_args);
通過調用本方法,可以得到VM的預設配置屬性。方法入參為指向JavaVMInitArgs類型對象的指標,在本地代碼調用JNI_GetDefaultJavaVMInitArgs方法前需要設定期望VM支援的版本號碼。方法調用返回值為JNI_OK時表示成功,VM會將版本號碼設定為實際支援的值;方法的返回值非JNI_OK時,表示調用失敗。
JNI_GetCreatedJavaVMs簽名為jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);
通過調用本方法,可以擷取到當前已建立的全部VM對象。vmBuf為儲存指標的數組,長度由bufLen給出,而實際的VM數量將在nVMs變數中返回。
單個進程中不允許建立多個VM執行個體。
JNI_CreateJavaVM簽名為jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);
建立VM對象,並完成必要的初始化操作,同時調用方法的線程被設定為主線程。在單個進程中不允許建立多個VM對象。
DestroyJavaVM簽名為jint DestroyJavaVM(JavaVM *vm);
通過調用本方法,可以卸載已建立的VM對象,並回收相關的資源。本方法是安全執行緒的,可以在任意線程使用。本方法在使用時會阻塞當前線程嗎?假如調用線程沒有與VM對象建立了關聯關係,則直接建立關聯關係,然後等待其它使用者線程退出;假如已建立了關聯關係,則直接等待其它使用者線程退出。
Unloading of the VM is not supported.這語沒有看明白什麼意思。
AttachCurrentThread簽名jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);
當前調用線程與VM建立關聯關係,擷取到可在當前安全執行緒使用的JNIEnv對象。假如當前線程已經與VM建立關聯,多次調用本方法是安全的。本地線程在同一時段內,只能與一個VM對象建立關聯關係(這個說法很奇怪,之前的資源提示在單個進程內,只允許建立一個VM,這裡為什麼又提示說避免與多個VM關聯?)。
當本地線程與VM建立關聯之後,線程使用的上下文類載入器將是VM的啟動類載入器。
調用本方法時,第一個參數為指向VM對象的指標,第二個參數為JNIEnv類型對象的指標,第三個參數為JavaVMAttachArgs類型對象的指標,但沒有實際用途,應當為設定為NULL(不過原文檔對這個參數的介紹稍有點混亂,需要實際驗證一下)。
AttachCurrentThreadAsDaemon簽名jint AttachCurrentThreadAsDaemon(JavaVM* vm, void** penv, void* args);
用法和參數與AttachCurrentThread方法類似,區別在於VM內部建立的java.lang.Thread對象將會是一個daemon。如果當前本地線程已經和VM建立關聯,則多次調用AttachCurrentThread或者AttachCurrentThreadAsDaemon並不會修改java.lang.Thread對象的daemon屬性。
DetachCurrentThread簽名jint DetachCurrentThread(JavaVM *vm);
當前線程與VM解除關聯,本地線程持有的鎖對象將全部釋放。等待當前線程的Java線程將得到通知。主線程通過調用本方法,可以和VM解除關聯。
GetEnv簽名為jint GetEnv(JavaVM *vm, void **env, jint version);
通過調用本方法,可以擷取到當前線程的JNIEnv對象。如果當前線程與VM沒有建立關聯,則*env被設定為NULL,同時返回JNI_EDETACHED;如果傳入的VM特性版本號碼不被支援,則*env被設定為NULL,同時返回JNI_EVERSION;如果調用成功,則*env被設定為正確的JNIEnv對象指標,同時返回JNI_OK。