Java本地接口規(guī)范設(shè)計(jì)概述 收藏
平臺(tái)相關(guān)代碼是通過調(diào)用 JNI 函數(shù)來訪問 Java 虛擬機(jī)功能的。JNI 函數(shù)可通過接口指針來獲得。接口指針是指針的指針,它指向一個(gè)指針數(shù)組,而指針數(shù)組中的每個(gè)元素又指向一個(gè)接口函數(shù)。每個(gè)接口函數(shù)都處在數(shù)組的某個(gè)預(yù)定偏移量中。圖 2-1 說明了接口指針的組織結(jié)構(gòu)。
圖 2-1 接口指針
JNI 接口的組織類似于 C++ 虛擬函數(shù)表或 COM 接口。使用接口表而不使用硬性編入的函數(shù)表的好處是使 JNI 名字空間與平臺(tái)相關(guān)代碼分開。虛擬機(jī)可以很容易地提供多個(gè)版本的 JNI 函數(shù)表。例如,虛擬機(jī)可支持以下兩個(gè) JNI 函數(shù)表:
一個(gè)表對(duì)非法參數(shù)進(jìn)行全面檢查,適用于調(diào)試程序;
另一個(gè)表只進(jìn)行 JNI 規(guī)范所要求的最小程度的檢查,因此效率較高。
JNI 接口指針只在當(dāng)前線程中有效。因此,本地方法不能將接口指針從一個(gè)線程傳遞到另一個(gè)線程中。實(shí)現(xiàn) JNI 的虛擬機(jī)可將本地線程的數(shù)據(jù)分配和儲(chǔ)存在 JNI 接口指針?biāo)赶虻膮^(qū)域中。
本地方法將 JNI 接口指針當(dāng)作參數(shù)來接受。虛擬機(jī)在從相同的 Java 線程中對(duì)本地方法進(jìn)行多次調(diào)用時(shí),保證傳遞給該本地方法的接口指針是相同的。但是,一個(gè)本地方法可被不同的 Java 線程所調(diào)用,因此可以接受不同的 JNI 接口指針。
--------------------------------------------------------------------------------
加載和鏈接本地方法
對(duì)本地方法的加載通過 System.loadLibrary 方法實(shí)現(xiàn)。下例中,類初始化方法加載了一個(gè)與平臺(tái)有關(guān)的本地庫,在該本地庫中給出了本地方法 f 的定義:
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary("pkg_Cls");
}
}
System.loadLibrary 的參數(shù)是程序員任意選取的庫名。系統(tǒng)按照標(biāo)準(zhǔn)的但與平臺(tái)有關(guān)的處理方法將該庫名轉(zhuǎn)換為本地庫名。例如,Solaris 系統(tǒng)將名稱 pkg_Cls 轉(zhuǎn)換為 libpkg_Cls.so,而 Win32 系統(tǒng)將相同的名稱 pkg_Cls 轉(zhuǎn)換為 pkg_Cls.dll。
程序員可用單個(gè)庫來存放任意數(shù)量的類所需的所有本地方法,只要這些類將被相同的類加載器所加載。虛擬機(jī)在其內(nèi)部為每個(gè)類加載器保護(hù)其所加載的本地庫清單。提供者應(yīng)該盡量選擇能夠避免名稱沖突的本地庫名。
如果底層操作系統(tǒng)不支持動(dòng)態(tài)鏈接,則必須事先將所有的本地方法鏈接到虛擬機(jī)上。這種情況下,虛擬機(jī)實(shí)際上不需要加載庫即可完成 System.loadLibrary 調(diào)用。
程序員還可調(diào)用 JNI 函數(shù) RegisterNatives() 來注冊(cè)與類關(guān)聯(lián)的本地方法。在與靜態(tài)鏈接的函數(shù)一起使用時(shí),RegisterNatives() 函數(shù)將特別有用。
解析本地方法名
動(dòng)態(tài)鏈接程序是根據(jù)項(xiàng)的名稱來解析各項(xiàng)的。本地方法名由以下幾部分串接而成:
前綴 Java_
mangled 全限定的類名
下劃線(“_”)分隔符
mangled 方法名
對(duì)于重載的本地方法,加上兩個(gè)下劃線(“__”),后跟 mangled 參數(shù)簽名
虛擬機(jī)將為本地庫中的方法查找匹配的方法名。它首先查找短名(沒有參數(shù)簽名的名稱),然后再查找?guī)?shù)簽名的長名稱。只有當(dāng)某個(gè)本地方法被另一個(gè)本地方法重載時(shí)程序員才有必要使用長名。但如果本地方法的名稱與非本地方法的名稱相同,則不會(huì)有問題。因?yàn)榉潜镜胤椒ǎ↗ava 方法)并不放在本地庫中。
下例中,不必用長名來鏈接本地方法 g,因?yàn)榱硪粋€(gè)方法 g 不是本地方法,因而它并不在本地庫中。
class Cls1 {
int g(int i);
native int g(double d);
}
我們采取簡單的名字?jǐn)噥y方案,以保證所有的 Unicode 字符都能被轉(zhuǎn)換為有效的 C 函數(shù)名。我們用下劃線(“_”)字符來代替全限定的類名中的斜杠(“/”)。由于名稱或類型描述符從來不會(huì)以數(shù)字打頭,我們用 _0、...、_9 來代替轉(zhuǎn)義字符序列,如表 2-1 所示:
表 2-1 Unicode 字符轉(zhuǎn)換
轉(zhuǎn)義字符序列 表示
_0XXXX Unicode 字符 XXXX。
_1 字符“_”
_2 簽名中的字符“;”
_3 簽名中的字符“[”
本地方法和接口 API 都要遵守給定平臺(tái)上的庫調(diào)用標(biāo)準(zhǔn)約定。例如,UNIX 系統(tǒng)使用 C 調(diào)用約定,而 Win32 系統(tǒng)使用 __stdcall。
本地方法的參數(shù)
JNI 接口指針是本地方法的第一個(gè)參數(shù)。其類型是 JNIEnv。第二個(gè)參數(shù)隨本地方法是靜態(tài)還是非靜態(tài)而有所不同。非靜態(tài)本地方法的第二個(gè)參數(shù)是對(duì)對(duì)象的引用,而靜態(tài)本地方法的第二個(gè)參數(shù)是對(duì)其 Java 類的引用。
其余的參數(shù)對(duì)應(yīng)于通常 Java 方法的參數(shù)。本地方法調(diào)用利用返回值將結(jié)果傳回調(diào)用程序中。第 3 章 “JNI 的類型和數(shù)據(jù)結(jié)構(gòu)” 將描述 Java 類型和 C 類型之間的映射。
代碼示例 2-1 說明了如何用 C 函數(shù)來實(shí)現(xiàn)本地方法 f。對(duì)本地方法 f 的聲明如下:
package pkg;
class Cls {
native double f(int i, String s);
...
}
具有長 mangled 名稱 Java_pkg_Cls_f_ILjava_lang_String_2 的 C 函數(shù)實(shí)現(xiàn)本地方法f:
代碼示例 2-1: 用 C 實(shí)現(xiàn)本地方法
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* 接口指針 */
jobject obj, /* “this”指針 */
jint i, /* 第一個(gè)參數(shù) */
jstring s) /* 第二個(gè)參數(shù) */
{
/* 取得 Java 字符串的 C 版本 */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* 處理該字符串 */
...
/* 至此完成對(duì) str 的處理 */
(*env)->ReleaseStringUTFChars(env, s, str);
return ...
}
注意,我們總是用接口指針 env 來操作 Java 對(duì)象??捎?C++ 將此代碼寫得稍微簡潔一些,如代碼示例 2-2 所示:
代碼示例 2-2: 用 C++ 實(shí)現(xiàn)本地方法
extern "C" /* 指定 C 調(diào)用約定 */
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* 接口指針 */
jobject obj, /* “this”指針 */
jint i, /* 第一個(gè)參數(shù) */
jstring s) /* 第二個(gè)參數(shù) */
{
const char *str = env->GetStringUTFChars(s, 0);
...
env->ReleaseStringUTFChars(s, str);
return ...
}
使用 C++ 后,源代碼變得更為直接,且接口指針參數(shù)消失。但是,C++ 的內(nèi)在機(jī)制與 C 的完全一樣。在 C++ 中,JNI 函數(shù)被定義為內(nèi)聯(lián)成員函數(shù),它們將擴(kuò)展為相應(yīng)的 C 對(duì)應(yīng)函數(shù)。
--------------------------------------------------------------------------------
引用 Java 對(duì)象
基本類型(如整型、字符型等)在 Java 和平臺(tái)相關(guān)代碼之間直接進(jìn)行復(fù)制。而 Java 對(duì)象由引用來傳遞。虛擬機(jī)必須跟蹤傳到平臺(tái)相關(guān)代碼中的對(duì)象,以使這些對(duì)象不會(huì)被垃圾收集器釋放。反之,平臺(tái)相關(guān)代碼必須能用某種方式通知虛擬機(jī)它不再需要那些對(duì)象,同時(shí),垃圾收集器必須能夠移走被平臺(tái)相關(guān)代碼引用過的對(duì)象。
全局和局部引用
JNI 將平臺(tái)相關(guān)代碼使用的對(duì)象引用分成兩類:局部引用和全局引用。局部引用在本地方法調(diào)用期間有效,并在本地方法返回后被自動(dòng)釋放掉。全局引用將一直有效,直到被顯式釋放。
對(duì)象是被作為局部引用傳遞給本地方法的,由 JNI 函數(shù)返回的所有 Java 對(duì)象也都是局部引用。JNI 允許程序員從局部引用創(chuàng)建全局引用。要求 Java 對(duì)象的 JNI 函數(shù)既可接受全局引用也可接受局部引用。本地方法將局部引用或全局引用作為結(jié)果返回。
大多數(shù)情況下,程序員應(yīng)該依靠虛擬機(jī)在本地方法返回后釋放所有局部引用。但是,有時(shí)程序員必須顯式釋放某個(gè)局部引用。例如,考慮以下的情形:
本地方法要訪問一個(gè)大型 Java 對(duì)象,于是創(chuàng)建了對(duì)該 Java 對(duì)象的局部引用。然后,本地方法要在返回調(diào)用程序之前執(zhí)行其它計(jì)算。對(duì)這個(gè)大型 Java 對(duì)象的局部引用將防止該對(duì)象被當(dāng)作垃圾收集,即使在剩余的運(yùn)算中并不再需要該對(duì)象。
本地方法創(chuàng)建了大量的局部引用,但這些局部引用并不是要同時(shí)使用。由于虛擬機(jī)需要一定的空間來跟蹤每個(gè)局部引用,創(chuàng)建太多的局部引用將可能使系統(tǒng)耗盡內(nèi)存。例如,本地方法要在一個(gè)大型對(duì)象數(shù)組中循環(huán),把取回的元素作為局部引用,并在每次迭代時(shí)對(duì)一個(gè)元素進(jìn)行操作。每次迭代后,程序員不再需要對(duì)該數(shù)組元素的局部引用。
JNI 允許程序員在本地方法內(nèi)的任何地方對(duì)局部引用進(jìn)行手工刪除。為確保程序員可以手工釋放局部引用,JNI 函數(shù)將不能創(chuàng)建額外的局部引用,除非是這些 JNI 函數(shù)要作為結(jié)果返回的引用。
局部引用僅在創(chuàng)建它們的線程中有效。本地方法不能將局部引用從一個(gè)線程傳遞到另一個(gè)線程中。
實(shí)現(xiàn)局部引用
為了實(shí)現(xiàn)局部引用,Java 虛擬機(jī)為每個(gè)從 Java 到本地方法的控制轉(zhuǎn)換都創(chuàng)建了注冊(cè)服務(wù)程序。注冊(cè)服務(wù)程序?qū)⒉豢梢苿?dòng)的局部引用映射為 Java 對(duì)象,并防止這些對(duì)象被當(dāng)作垃圾收集。所有傳給本地方法的 Java 對(duì)象(包括那些作為 JNI 函數(shù)調(diào)用結(jié)果返回的對(duì)象)將被自動(dòng)添加到注冊(cè)服務(wù)程序中。本地方法返回后,注冊(cè)服務(wù)程序?qū)⒈粍h除,其中的所有項(xiàng)都可以被當(dāng)作垃圾來收集。
可用各種不同的方法來實(shí)現(xiàn)注冊(cè)服務(wù)程序,例如,使用表、鏈接列表或 hash 表來實(shí)現(xiàn)。雖然引用計(jì)數(shù)可用來避免注冊(cè)服務(wù)程序中有重復(fù)的項(xiàng),但 JNI 實(shí)現(xiàn)不是必須檢測(cè)和消除重復(fù)的項(xiàng)。
注意,以保守方式掃描本地堆棧并不能如實(shí)地實(shí)現(xiàn)局部引用。平臺(tái)相關(guān)代碼可將局部引用儲(chǔ)存在全局或堆數(shù)據(jù)結(jié)構(gòu)中。
--------------------------------------------------------------------------------
訪問 Java 對(duì)象
JNI 提供了一大批用來訪問全局引用和局部引用的函數(shù)。這意味著無論虛擬機(jī)在內(nèi)部如何表示 Java 對(duì)象,相同的本地方法實(shí)現(xiàn)都能工作。這就是為什么 JNI 可被各種各樣的虛擬機(jī)實(shí)現(xiàn)所支持的關(guān)鍵原因。
通過不透明的引用來使用訪問函數(shù)的開銷比直接訪問 C 數(shù)據(jù)結(jié)構(gòu)的開銷來得高。我們相信,大多數(shù)情況下,Java 程序員使用本地方法是為了完成一些重要任務(wù),此時(shí)這種接口的開銷不是首要問題。
訪問基本類型數(shù)組
對(duì)于含有大量基本數(shù)據(jù)類型(如整數(shù)數(shù)組和字符串)的 Java 對(duì)象來說,這種開銷將高得不可接受 (考慮一下用于執(zhí)行矢量和矩陣運(yùn)算的本地方法的情形便知)。對(duì) Java 數(shù)組進(jìn)行迭代并且要通過函數(shù)調(diào)用取回?cái)?shù)組的每個(gè)元素,其效率是非常低的。
一個(gè)解決辦法是引入“釘住”概念,以使本地方法能夠要求虛擬機(jī)釘住數(shù)組內(nèi)容。而后,該本地方法將接受指向數(shù)值元素的直接指針。但是,這種方法包含以下兩個(gè)前提:
垃圾收集器必須支持釘住。
虛擬機(jī)必須在內(nèi)存中連續(xù)存放基本類型數(shù)組。雖然大多數(shù)基本類型數(shù)組都是連續(xù)存放的,但布爾數(shù)組可以壓縮或不壓縮存儲(chǔ)。因此,依賴于布爾數(shù)組確切存儲(chǔ)方式的本地方法將是不可移植的。
我們將采取折衷方法來克服上述兩個(gè)問題。
首先,我們提供了一套函數(shù),用于在 Java 數(shù)組的一部分和本地內(nèi)存緩沖之間復(fù)制基本類型數(shù)組元素。這些函數(shù)只有在本地方法只需訪問大型數(shù)組中的一小部分元素時(shí)才使用。
其次,程序員可用另一套函數(shù)來取回?cái)?shù)組元素的受約束版本。記住,這些函數(shù)可能要求 Java 虛擬機(jī)分配存儲(chǔ)空間和進(jìn)行復(fù)制。虛擬機(jī)實(shí)現(xiàn)將決定這些函數(shù)是否真正復(fù)制該數(shù)組,如下所示:
如果垃圾收集器支持釘住,且數(shù)組的布局符合本地方法的要求,則不需要進(jìn)行復(fù)制。
否則,該數(shù)組將被復(fù)制到不可移動(dòng)的內(nèi)存塊中(例如,復(fù)制到 C 堆中),并進(jìn)行必要的格式轉(zhuǎn)換,然后返回指向該副本的指針。
最后,接口提供了一些函數(shù),用以通知虛擬機(jī)本地方法已不再需要訪問這些數(shù)組元素。當(dāng)調(diào)用這些函數(shù)時(shí),系統(tǒng)或者釋放數(shù)組,或者在原始數(shù)組與其不可移動(dòng)副本之間進(jìn)行協(xié)調(diào)并將副本釋放。
這種處理方法具有靈活性。垃圾收集器的算法可對(duì)每個(gè)給定的數(shù)組分別作出復(fù)制或釘住的決定。例如,垃圾收集器可能復(fù)制小型對(duì)象而釘住大型對(duì)象。
JNI 實(shí)現(xiàn)必須確保多個(gè)線程中運(yùn)行的本地方法可同時(shí)訪問同一數(shù)組。例如,JNI 可以為每個(gè)被釘住的數(shù)組保留一個(gè)內(nèi)部計(jì)數(shù)器,以便某個(gè)線程不會(huì)解開同時(shí)被另一個(gè)線程釘住的數(shù)組。注意,JNI 不必將基本類型數(shù)組鎖住以專供某個(gè)本地方法訪問。同時(shí)從不同的線程對(duì) Java 數(shù)組進(jìn)行更新將導(dǎo)致不確定的結(jié)果。
訪問域和方法
JNI 允許本地方法訪問 Java 對(duì)象的域或調(diào)用其方法。JNI 用符號(hào)名稱和類型簽名來識(shí)別方法和域。從名稱和簽名來定位域或?qū)ο蟮倪^程可分為兩步。例如,為調(diào)用類 cls 中的 f 方法,平臺(tái)相關(guān)代碼首先要獲得方法 ID,如下所示:
jmethodID mid =
env->GetMethodID(cls, "f", "(ILjava/lang/String;)D");
然后,平臺(tái)相關(guān)代碼可重復(fù)使用該方法 ID 而無須再查找該方法,如下所示:
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
域 ID 或方法 ID 并不能防止虛擬機(jī)卸載生成該 ID 的類。該類被卸載之后,該方法 ID 或域 ID 亦變成無效。因此,如果平臺(tái)相關(guān)代碼要長時(shí)間使用某個(gè)方法 ID 或域 ID,則它必須確保:
保留對(duì)所涉及類的活引用,或
重新計(jì)算該方法 ID 或域 ID。
JNI 對(duì)域 ID 和方法 ID 的內(nèi)部實(shí)現(xiàn)并不施加任何限制。
--------------------------------------------------------------------------------
報(bào)告編程錯(cuò)誤
JNI 不檢查諸如傳遞 NULL 指針或非法參數(shù)類型之類的編程錯(cuò)誤。非法的參數(shù)類型包括諸如要用 Java 類對(duì)象時(shí)卻用了普通 Java 對(duì)象這樣的錯(cuò)誤。JNI 不檢查這些編程錯(cuò)誤的理由如下:
強(qiáng)迫 JNI 函數(shù)去檢查所有可能的錯(cuò)誤情況將降低正常(正確)的本地方法的性能。
在許多情況下,沒有足夠的運(yùn)行時(shí)的類型信息可供這種檢查使用。
大多數(shù) C 庫函數(shù)對(duì)編程錯(cuò)誤不進(jìn)行防范。例如,printf() 函數(shù)在接到一個(gè)無效地址時(shí)通常是引起運(yùn)行錯(cuò)而不是返回錯(cuò)誤代碼。強(qiáng)迫 C 庫函數(shù)檢查所有可能的錯(cuò)誤情況將有可能引起這種檢查被重復(fù)進(jìn)行--先是在用戶代碼中進(jìn)行,然后又在庫函數(shù)中再次進(jìn)行。
程序員不得將非法指針或錯(cuò)誤類型的參數(shù)傳遞給 JNI 函數(shù)。否則,可能產(chǎn)生意想不到的后果,包括可能使系統(tǒng)狀態(tài)受損或使虛擬機(jī)崩潰。
--------------------------------------------------------------------------------
Java 異常
JNI 允許本地方法拋出任何 Java 異常。本地方法也可以處理突出的 Java 異常。未被處理的 Java 異常將被傳回虛擬機(jī)中。
異常和錯(cuò)誤代碼
一些 JNI 函數(shù)使用 Java 異常機(jī)制來報(bào)告錯(cuò)誤情況。大多數(shù)情況下,JNI 函數(shù)通過返回錯(cuò)誤代碼并拋出 Java 異常來報(bào)告錯(cuò)誤情況。錯(cuò)誤代碼通常是特殊的返回值(如 NULL),這種特殊的返回值在正常返回值范圍之外。因此,程序員可以:
快速檢查上一個(gè) JNI 調(diào)用所返回的值以確定是否出錯(cuò),并
通過調(diào)用函數(shù) ExceptionOccurred() 來獲得異常對(duì)象,它含有對(duì)錯(cuò)誤情況的更詳細(xì)說明。
在以下兩種情況中,程序員需要先查出異常,然后才能檢查錯(cuò)誤代碼:
調(diào)用 Java 方法的 JNI 函數(shù)返回該 Java 方法的結(jié)果。程序員必須調(diào)用 ExceptionOccurred() 以檢查在執(zhí)行 Java 方法期間可能發(fā)生的異常。
某些用于訪問 JNI 數(shù)組的函數(shù)并不返回錯(cuò)誤代碼,但可能會(huì)拋出 ArrayIndexOutOfBoundsException 或 ArrayStoreException。
在所有其它情況下,返回值如果不是錯(cuò)誤代碼值就可確保沒有拋出異常。
異步異常
在多個(gè)線程的情況下,當(dāng)前線程以外的其它線程可能會(huì)拋出異步異常。異步異常并不立即影響當(dāng)前線程中平臺(tái)相關(guān)代碼的執(zhí)行,直到出現(xiàn)下列情況:
該平臺(tái)相關(guān)代碼調(diào)用某個(gè)有可能拋出同步異常的 JNI 函數(shù),或者
該平臺(tái)相關(guān)代碼用 ExceptionOccurred() 顯式檢查同步異?;虍惒疆惓!?
注意,只有那些有可能拋出同步異常的 JNI 函數(shù)才檢查異步異常。
本地方法應(yīng)在必要的地方(例如,在一個(gè)沒有其它異常檢查的緊密循環(huán)中)插入 ExceptionOccurred() 檢查以確保當(dāng)前線程可在適當(dāng)時(shí)間內(nèi)對(duì)異步異常作出響應(yīng)。
異常的處理
可用兩種方法來處理平臺(tái)相關(guān)代碼中的異常:
本地方法可選擇立即返回,使異常在啟動(dòng)該本地方法調(diào)用的 Java 代碼中拋出。
平臺(tái)相關(guān)代碼可通過調(diào)用 ExceptionClear() 來清除異常,然后執(zhí)行自己的異常處理代碼。
拋出了某個(gè)異常之后,平臺(tái)相關(guān)代碼必須先清除異常,然后才能進(jìn)行其它的 JNI 調(diào)用。當(dāng)有待定異常時(shí),只有以下這些 JNI 函數(shù)可被安全地調(diào)用:ExceptionOccurred()、ExceptionDescribe() 和 ExceptionClear()。ExceptionDescribe() 函數(shù)將打印有關(guān)待定異常的調(diào)試消息。