上一篇:01-導言
????????本章重點討論 JNI 中的主要設計問題。本節中的大多數設計問題都與本地方法有關。調用 API 的設計將在第 5 章:調用 API 中介紹。
2.1?JNI 接口函數和指針
????????本地代碼通過調用 JNI 函數來訪問 Java 虛擬機功能。JNI 函數可通過接口指針使用。接口指針是指向指針的指針。該指針指向一個指針數組,數組中每個指針指向一個接口函數。每個接口函數都位于數組內預定義的偏移量處。下圖 "接口指針 "說明了接口指針的組織結構。
????????JNI 接口的組織方式類似于 C++ 虛擬函數表或 COM 接口。使用接口表而不是硬連線函數項的好處是,JNI 名稱空間與本地代碼分離。虛擬機可以輕松提供多個版本的 JNI 函數表。例如,虛擬機可以支持兩個 JNI 函數表:
? ? ? ? ①. 一種會對非法參數進行徹底檢查,適合調試;
????????②.?而另一種方法只執行 JNI 規范要求的最少檢查量,因此效率更高。
????????JNI 接口指針只在當前線程中有效。因此,本地方法不得將接口指針從一個線程傳遞到另一個線程。實現 JNI 的虛擬機可以在 JNI 接口指針指向的區域中分配和存儲線程本地數據。
????????本地方法接收 JNI 接口指針作為參數。當虛擬機從同一個 Java 線程多次調用本地方法時,會保證將同一個接口指針傳遞給本地方法。不過,本地方法可以從不同的 Java 線程調用,因此可能會收到不同的 JNI 接口指針。
2.2?編譯、加載和鏈接本地方法
????????由于 Java 虛擬機是多線程的,因此本地庫也應使用多線程感知本地編譯器進行編譯和鏈接。例如,使用 Sun Studio 編譯器編譯的 C++ 代碼應使用 -mt 標志。對于使用 GNU gcc 編譯器編譯的代碼,應使用標志 -D_REENTRANT 或 -D_POSIX_C_SOURCE 。
????????本地方法用 System.loadLibrary 方法加載。在下面的示例中,類初始化方法加載了一個特定平臺的本地庫,其中定義了本地方法 f :
package p.q.r;class A {native double f(int i, String s);static {// // /*** defined in "libcore/ojluni/src/main/java/java/lang/System.java"* public static void loadLibrary(String libname);* 加載由libname參數指定的本機庫。* libname參數不能包含任何特定于平臺的前綴、文件擴展名或路徑。* 如果一個名為libname的本地庫與虛擬機靜態鏈接,則調用該庫導出的JNI_OnLoad()函數。* 否則,libname參數將從系統庫位置加載,并以依賴于實現的方式映射到本機庫映像。*/System.loadLibrary("p_q_r_A");}
}
????????System.loadLibrary 的參數是程序員任意選擇的庫名。系統會采用一種標準的、但與特定平臺有關的方法,將庫名稱轉換為本地庫名稱。例如,Linux 系統會將庫名 p_q_r_A 轉換為 libp_q_r_A.so ,而 Windows 系統會將庫名 p_q_r_A 轉換為 p_q_r_A.dll 。
????????程序員可以使用一個庫來存儲任意數量的類所需的所有本地方法,只要這些類是用同一個類加載器加載的。虛擬機內部會為每個類加載器維護一個已加載的本地庫列表。供應商在選擇本地庫名稱時,應盡量減少名稱沖突的機會。
????????對動態鏈接和靜態鏈接庫的支持,以及它們各自的生命周期管理 "加載 "和 "卸載 "功能鉤子,在庫和版本管理的調用 API 部分有詳細介紹。
2.2.1 解析本地方法名稱
????????JNI 定義了從 Java 中聲明的 native 方法名稱到本地庫中本地方法名稱的 1:1 映射。虛擬機使用該映射將 native 方法的 Java 調用動態鏈接到本地庫中的相應實現。
????????該映射通過連接從 native 方法聲明中派生出來的以下組件來生成本地方法名稱:
? ? ? ? ? ? ? ? ①.?Java_前綴;
????????????????②.?給出了聲明 native 方法的類的二進制內部名稱:轉義該名稱的結果。
????????????????③.??("_") 下劃線
????????????????④.?轉義方法名
????????????????⑤.?如果 native 方法聲明是重載的:兩個下劃線("__"),后跟方法聲明的轉義參數描述符(JVMS 4.3.3)。
????????轉義時,每個字母數字 ASCII 字符 ( A-Za-z0-9 ) 都保持不變,并用相應的轉義序列替換下表中的每個 UTF-16 代碼單元。如果要轉義的名稱包含一對代用字符,則高代用代碼單元和低代用代碼單元將分別轉義。轉義的結果是一個僅由 ASCII 字符 A-Za-z0-9 和下劃線組成的字符串。
????????出于兩個原因,轉義是必要的。首先,為了確保 Java 源代碼中可能包含 Unicode 字符的類名和方法名能轉換成 C 源代碼中有效的函數名。其次,確保 native 方法的參數描述符(使用"; "和"["字符對參數類型進行編碼)可以在 C 函數名中編碼。
????????當 Java 程序調用一個 native 方法時,虛擬機會搜索本地庫,首先查找本地方法名稱的簡短版本,即不包含轉義參數簽名的名稱。如果找不到短名稱的本地方法,虛擬機就會查找長版本的本地方法名稱,即包含轉義參數簽名的名稱。
????????先查找簡短的名稱,可以更方便地在本地庫中聲明實現。例如,給出 Java 中的 native 方法:
package p.q.r;
class A {native double f(int i, String s);
}
????????相應的 C 函數可以命名為 Java_p_q_r_A_f ,而不是 Java_p_q_r_A_f__ILjava_lang_String_2 。
????????只有當一個類中的兩個或多個 native 方法具有相同的名稱時,才有必要在本地庫中聲明具有長名稱的實現。例如,Java 中有以下1 個方法:
package p.q.r;
class A {native double f(int i, String s);native double f(int i, Object s);
}
????????相應的 C 語言函數必須命名為 Java_p_q_r_A_f__ILjava_lang_String_2 和 Java_p_q_r_A_f__ILjava_lang_Object_2 ,因為這兩個方法是重載的。
????????如果 Java 中的 native 方法只被非 native 方法重載,則無需在本地庫中使用長名稱。在下面的示例中, native 方法 g 不必使用長名稱鏈接,因為另一個方法 g 不是 native 方法,因此不存在于本地庫中。
package p.q.r;
class B {int g(int i);native int g(double d);
}
????????請注意,轉義序列可以安全地以 _0 、 _1 等開頭,因為 Java 源代碼中的類名和方法名從不以數字開頭。但是,在非 Java 源代碼生成的類文件中,情況并非如此。為了保持與本地方法名稱的 1:1 映射,虛擬機會對生成的名稱進行如下檢查。如果從方法聲明(類或方法名稱或參數類型)中轉義任何前導字符串的過程導致前導字符串中的 " 0 "、" 1 "、" 2 "或 " 3 "字符在結果中緊跟下劃線后或在轉義字符串的開頭(在完全組裝后的名稱中,這些字符將緊跟下劃線)出現,且未發生變化,則稱轉義過程 "失敗"。在這種情況下,將不執行本地庫搜索,并且在嘗試鏈接 native 方法調用時將拋出 UnsatisfiedLinkError 。可以擴展目前的簡單映射方案以涵蓋這種情況,但復雜性成本將超過任何好處。
????????本地方法和接口 API 都遵循特定平臺上的標準庫調用約定。例如,UNIX 系統使用 C 調用約定,而 Win32 系統使用 __stdcall。
????????本機方法也可以使用 RegisterNatives 功能進行顯式鏈接。需要注意的是, RegisterNatives 函數可以通過更改給定本地 Java 方法要執行的本地代碼來改變 JVM 的記錄行為(包括加密算法、正確性、安全性、類型安全性)。因此,請謹慎使用使用 RegisterNatives 函數的本地庫應用程序。
2.2.2 本地方法參數
????????JNI 接口指針是本地方法的第一個參數。JNI 接口指針的類型是 JNIEnv。第二個參數根據本地方法是靜態還是非靜態而有所不同。非靜態本地方法的第二個參數是對象的引用。靜態本地方法的第二個參數是對其 Java 類的引用。
????????其余參數與常規 Java 方法參數相對應。本地方法調用通過返回值將結果傳回調用例程。第 3 章:JNI 類型和數據結構,介紹了 Java 和 C 語言類型之間的映射。
????????下面的代碼示例說明了如何使用 C 語言函數來實現本地方法 f 。本地方法 f 的聲明如下:
package p.q.r;class A {native double f(int i, String s);// ...
}
????????長名稱為 Java_p_q_r_A_f_ILjava_lang_String_2 的 C 語言函數實現了本地方法 f :
jdouble Java_p_q_r_A_f__ILjava_lang_String_2 (JNIEnv *env, /* interface pointer */jobject obj, /* "this" pointer */jint i, /* argument #1 */jstring s) /* argument #2 */
{/* Obtain a C-copy of the Java string */const char *str = (*env)->GetStringUTFChars(env, s, 0);/* process the string */.../* Now we are done with str */(*env)->ReleaseStringUTFChars(env, s, str);return ...
}
????????請注意,我們總是使用接口指針 env 來操作 Java 對象。如下面的代碼示例所示,使用 C++ 可以編寫稍微簡潔的代碼:
extern "C" /* specify the C calling convention */jdouble Java_p_q_r_A_f__ILjava_lang_String_2 (JNIEnv *env, /* interface pointer */jobject obj, /* "this" pointer */jint i, /* argument #1 */jstring s) /* argument #2 */{const char *str = env->GetStringUTFChars(s, 0);// ...env->ReleaseStringUTFChars(s, str);// return ...
}
????????在 C++ 中,額外的間接層次和接口指針參數從源代碼中消失了。在 C++ 中,JNI 函數被定義為內聯成員函數,可擴展為 C 語言的對應函數。
2.3?引用 Java 對象
????????原始類型(如整數、字符等)可在 Java 和本地代碼之間復制。而任意 Java 對象則通過引用傳遞。虛擬機必須跟蹤所有已傳遞給本地代碼的對象,以免垃圾回收器釋放這些對象。反過來,本地代碼也必須有辦法通知虛擬機它不再需要這些對象。此外,垃圾回收器必須能夠移動本地代碼引用的對象。
2.3.1 全局和本地引用
????????JNI 將本地代碼使用的對象引用分為兩類:本地引用和全局引用。本地引用在本地方法調用期間有效,并在本地方法返回后自動釋放。全局引用在顯式釋放之前一直有效。
????????對象以本地引用的形式傳遞給本地方法。JNI 函數返回的所有 Java 對象都是局部引用。JNI 允許程序員從局部引用創建全局引用。期望返回 Java 對象的 JNI 函數既接受全局引用,也接受局部引用。本地方法可將本地引用或全局引用作為結果返回給 VM。
????????在大多數情況下,程序員應依靠虛擬機在本地方法返回后釋放所有本地引用。不過,有時程序員應該顯式釋放本地引用。例如,請考慮以下情況:
? ? ? ? ? ? ? ? ①.?本地方法訪問大型 Java 對象,從而創建 Java 對象的本地引用。然后,本地方法在返回給調用者之前會執行額外的計算。即使在剩余的計算中不再使用該對象,該大型 Java 對象的本地引用也會阻止該對象被垃圾回收。
????????????????②.?本地方法會創建大量的本地引用,但并非所有引用都會同時使用。由于虛擬機需要一定的空間來跟蹤本地引用,創建過多的本地引用可能會導致系統內存不足。例如,一個本地方法在一個大型對象數組中循環,以本地引用的形式檢索元素,每次迭代對一個元素進行操作。每次迭代后,程序員就不再需要數組元素的本地引用了。
????????JNI 允許程序員在本地方法的任意位置手動刪除本地引用。為確保程序員能手動釋放本地引用,JNI 函數不允許創建額外的本地引用,但作為結果返回的引用除外。
????????本地引用只在創建引用的線程中有效。本地代碼不得將本地引用從一個線程傳遞到另一個線程。
2.3.2 本地引用的實現
????????為了實現本地引用,Java 虛擬機會為每次從 Java 到本地方法的控制轉換創建一個注冊表。注冊表將不可移動的本地引用映射到 Java 對象,并防止對象被垃圾回收。傳遞給本地方法的所有 Java 對象(包括作為 JNI 函數調用結果返回的對象)都會自動添加到注冊表中。本地方法返回后,注冊表將被刪除,從而允許對其所有條目進行垃圾回收。
????????實現注冊表的方法有很多種,例如:使用表、鏈表或哈希表。雖然可以使用引用計數來避免注冊表中的重復條目,但 JNI 實現沒有義務檢測和刪除重復條目。
????????請注意,本地引用不能通過保守地掃描本地堆棧來忠實地實現。本地代碼可能會將本地引用存儲到全局或堆數據結構中。
2.4?訪問 Java 對象
????????JNI 為全局和局部引用提供了豐富的訪問函數集。這意味著,無論虛擬機如何在內部表示 Java 對象,都能使用相同的本地方法實現。這也是 JNI 可以被各種虛擬機實現所支持的重要原因。
????????通過不透明引用使用訪問函數的開銷要高于直接訪問 C 數據結構的開銷。我們相信,在大多數情況下,Java 程序員會使用本地方法來執行一些非繁瑣的任務,而這些任務會掩蓋該接口的開銷。
2.4.1?訪問原始數組
? ? ? ? 對于包含許多原始數據類型(如整數數組和字符串)的大型 Java 對象來說,這種開銷是不可接受的。(考慮一下用于執行向量和矩陣計算的本地方法)。通過函數調用遍歷 Java 數組并檢索每個元素的效率非常低。
????????一種解決方案引入了 "鎖定 "的概念,這樣本地方法就可以要求虛擬機鎖定數組的內容。然后,本地方法會收到指向元素的直接指針。不過,這種方法有兩個影響:
? ? ? ? ? ? ? ? ①.?垃圾回收器必須支持“鎖住”功能。
????????????????②.?虛擬機必須在內存中連續布局基元數組。雖然這對大多數基元數組來說是最自然的實現方式,但布爾數組可以以打包或未打包的方式實現。因此,依賴于布爾數組精確布局的本地代碼將無法移植。
????????我們采取了一種折中的方法來克服上述兩個問題。
????????首先,我們提供了一組函數,用于在 Java 數組段和本地內存緩沖區之間復制原始數組元素。如果本地方法只需訪問大型數組中的少量元素,則使用這些函數。
????????其次,程序員可以使用另一組函數來檢索數組元素的固定版本。請記住,這些函數可能需要 Java 虛擬機執行存儲分配和復制。這些函數實際上是否復制數組取決于 VM 的實現,具體如下:
? ? ? ? ? ? ? ? ①.?如果垃圾回收器支持"鎖定",且數組布局與本地方法預期的相同,則無需復制。
????????????????②.?否則,數組將被復制到一個不可移動的內存塊(例如,在 C 堆中),并執行必要的格式轉換。系統將返回指向拷貝的指針。
????????最后,接口提供了一些函數,用于通知虛擬機本地代碼不再需要訪問數組元素。調用這些函數時,系統要么取消數組的鎖定,要么將原始數組與其不可移動的副本進行核對,并釋放副本。
????????我們的方法具有靈活性。垃圾回收器算法可以針對每個給定的數組分別做出復制或“鎖定”的決定。例如,垃圾回收器可以復制小對象,但“鎖定”較大的對象。
????????JNI 實現必須確保在多個線程中運行的本地方法可以同時訪問同一個數組。例如,JNI 可以為每個被釘住的數組保留一個內部計數器,這樣一個線程就不會解除被另一個線程釘住的數組。請注意,JNI 不需要為本地方法的獨占訪問鎖定原始數組。不同線程同時更新 Java 數組會導致非確定性結果。
2.4.2 訪問字段和方法
? ? ? ? JNI 允許本地代碼訪問 Java 對象的字段和調用 Java 對象的方法。JNI 通過符號名稱和類型簽名來識別方法和字段。根據字段或方法的名稱和簽名,分兩步計算出查找字段或方法的成本。例如,要調用類 cls 中的方法 f ,本地代碼首先要獲取一個方法 ID,如下所示:
jmethodID mid = env->GetMethodID(cls, "f", "(ILjava/lang/String;)D");
????????這樣,本地代碼就可以重復使用該方法 ID,而無需花費方法查找的成本,如下所示:
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
????????字段或方法 ID 不會阻止虛擬機卸銷毀 ID 所派生的類。類銷毀后,方法或字段 ID 將失效,并且不能傳遞給任何使用該 ID 的函數。因此,本地代碼如果打算長期使用某個方法或字段 ID,必須確保:
? ? ? ? ? ? ? ? ①.?保持對底層類的實時引用,或
????????????????②.?重新計算方法或字段 ID;
????????JNI 對內部如何實現字段和方法 ID 沒有施加任何限制。
2.4.2.1 調用對調用者敏感的方法
????????少數 Java 方法具有一種特殊屬性,稱為調用者敏感性。對調用者敏感的方法可以根據直接調用者的身份做出不同的行為。例如,AccessibleObject::canAccess 需要知道調用者才能確定是否可訪問。
????????當本地代碼調用此類方法時,調用棧上可能沒有任何 Java 調用者。程序員有責任了解從本地代碼調用的 Java 方法是否對調用者敏感,以及如果沒有 Java 調用者,這些方法將如何響應。如果有必要,程序員可以提供 Java 代碼供本地代碼調用,然后本地代碼再調用原始 Java 方法。
2.5 報告編程錯誤
????????JNI 不會檢查編程錯誤,如:傳遞 NULL 指針或非法參數類型。非法參數類型包括:使用普通 Java 對象而非 Java 類對象等。JNI 不檢查這些編程錯誤的原因如下:
? ? ? ? ? ? ? ? ①.?強制 JNI 函數檢查所有可能的錯誤條件會降低正常(正確)本地方法的性能。
????????????????②.?在許多情況下,沒有足夠的運行時類型信息來執行這種檢查。
????????多數 C 庫函數都不會防范編程錯誤。例如, printf() 函數在接收到無效地址時通常會導致運行時錯誤,而不是返回錯誤代碼。強制 C 庫函數檢查所有可能的錯誤條件很可能會導致重復檢查:在用戶代碼中檢查一次,然后在庫中再檢查一次。
????????程序員不得向 JNI 函數傳遞非法指針或錯誤類型的參數。否則可能導致任意后果,包括系統狀態損壞或虛擬機崩潰。
也就是說:傳遞這種空指針異常(nullptr)的檢查,由用戶負責;
2.6 Java異常
????????JNI 允許本地方法引發任意 Java 異常。本地代碼也可以處理未處理的 Java 異常。未處理的 Java 異常會傳播回虛擬機。
2.6.1?異常和錯誤代碼
????????某些 JNI 函數使用 Java 異常機制來報告錯誤條件。在大多數情況下,JNI 函數通過返回錯誤代碼和拋出 Java 異常來報告錯誤條件。錯誤代碼通常是超出正常返回值范圍的特殊返回值(如 NULL)。因此,程序員可以:
? ? ? ? ? ? ? ? ①.?快速檢查最后一次 JNI 調用的返回值,以確定是否發生錯誤,以及
????????????????②.?調用 ExceptionOccurred() 函數來獲取異常對象,該對象包含對錯誤條件的更詳細描述。
????????在兩種情況下,程序員需要檢查異常,而無法首先檢查錯誤代碼:
? ? ? ? ? ? ? ? ①.?調用 Java 方法的 JNI 函數會返回 Java 方法的結果。程序員必須調用 ExceptionOccurred() 來檢查 Java 方法執行過程中可能出現的異常。
????????????????②.?某些 JNI 數組訪問函數不會返回錯誤代碼,但可能會拋出 ArrayIndexOutOfBoundsException 或 ArrayStoreException 。
????????在所有其他情況下,非錯誤返回值保證沒有拋出異常。
2.6.2 異步異常
????????一個線程可以通過調用 Thread.stop() 方法在另一個線程中引發異步異常,該方法自 Java 2 SDK 1.2 版起已被棄用。強烈建議程序員不要使用 Thread.stop() ,因為它通常會導致應用程序狀態不確定。
????????此外,JVM 可能會在當前線程中產生異常,但這些異常并不是 JNI API 調用的直接結果,而是因為 JVM 內部的各種錯誤,例如: 像 StackOverflowError 或 OutOfMemoryError 的VirtualMachineError 。這些異常也被稱為異步異常。
????????異步異常不會立即影響當前線程中本地代碼的執行,直到出現以下情況:
? ? ? ? ? ? ? ? ①.?本地代碼調用了一個可能引發同步異常的 JNI 函數,或
????????????????②.?本地代碼使用 ExceptionOccurred() 來明確檢查同步和異步異常。
????????請注意,只有那些可能引發同步異常的 JNI 函數才會檢查異步異常。
????????本地方法應在必要的地方插入 ExceptionOccurred() 檢查,例如:在沒有其他異常檢查的任何長時間運行代碼中(可能包括緊密循環)。這樣可以確保當前線程在合理的時間內響應異步異常。不過,由于異步異常的特性,在調用前進行異常檢查并不能保證在檢查和調用之間不會引發異步異常。
2.6.3?異常處理
????????在本地代碼中有兩種處理異常的方法:
? ? ? ? ? ? ? ? ①.?本機方法可以選擇立即返回,從而在啟動本機方法調用的 Java 代碼中拋出異常。
????????????????②.?本地代碼可以通過調用 ExceptionClear() 清除異常,然后執行自己的異常處理代碼。
????????出現異常后,本地代碼必須先清除異常,然后才能調用其他 JNI 函數。當出現待處理異常時,可以安全調用的 JNI 函數有:
ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()
DetachCurrentThread()
下一篇: 03-JNI 類型和數據結構