Java本機介面規範內容 第2章:設計概述
本章重點介紹JNI中的主要設計問題。 本節中的大多數設計問題都與本機方法有關。
本章包括以下主題:
目錄
本機程式碼通過呼叫JNI函式來訪問Java VM功能。 JNI函式可通過介面指標獲得 。 介面指標是指向指標的指標。 該指標指向一個指標陣列,每個指標指向一個介面函式。 每個介面函式都在陣列內的預定義偏移處。 下圖“ 介面指標”說明了介面指標的組織。
該圖包含從左到右出現的三個元素,以及標有“介面函式”的三個橢圓:
- 文字:“JNI介面指標”
- 一列兩行的表:
- 第一行:“指標”
- 第二行:“每執行緒JNI資料結構”
- 標題為“指向JNI函式的指標陣列”的一列和四行的表:
- 第一行:“指標”。 箭頭從該表格單元格指向標記為“介面功能”的橢圓形。 沒有其他箭頭指向這個橢圓形。
- 第二行:“指標”。 箭頭從該表格單元格指向標記為“介面功能”的橢圓形。 沒有其他箭頭指向這個橢圓形。
- 第三行:“指標”。 箭頭從該表格單元格指向標記為“介面功能”的橢圓形。 沒有其他箭頭指向這個橢圓形。
- 第四行:“......”(省略號)
箭頭連線這三個元素:
- 從文字“JNI interace pointer”到表格單元格“Pointer”,從一列和兩行的表中
- 從表格單元格“指標”從一列和兩行的表到第一行(帶有文字“指標”)的表“指向JNI函式的指標陣列”
JNI介面的組織方式類似於C ++虛擬函式表或COM介面。 使用介面表而不是硬連線函式條目的優點是JNI名稱空間與本機程式碼分離。 VM可以輕鬆提供多個版本的JNI功能表。 例如,VM可能支援兩個JNI函式表:
- 一個人執行徹底的非法引數檢查,適合除錯;
- 另一個執行JNI規範所需的最小量檢查,因此更有效。
JNI介面指標僅在當前執行緒中有效。 因此,本機方法不能將介面指標從一個執行緒傳遞到另一個執行緒。 實現JNI的VM可以在JNI介面指標指向的區域中分配和儲存執行緒本地資料。
Native方法接收JNI介面指標作為引數。 當VM從同一Java執行緒多次呼叫本機方法時,保證將VM傳遞給本機方法。 但是,可以從不同的Java執行緒呼叫本機方法,因此可以接收不同的JNI介面指標。
由於Java VM是多執行緒的,因此本機庫也應該與多執行緒感知的本機編譯器一起編譯和連結。 例如, -mt
標誌應該用於使用Sun Studio編譯器編譯的C ++程式碼。 對於符合GNU gcc編譯器的程式碼,應使用標誌-D_REENTRANT
或-D_POSIX_C_SOURCE
。 有關更多資訊,請參閱本機編譯器文件。
本機方法使用System.loadLibrary
方法載入。 在以下示例中,類初始化方法載入特定於平臺的本機庫,其中定義了本機方法f
:
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary(“pkg_Cls”);
}
}
System.loadLibrary
的引數是程式設計師任意選擇的庫名。 系統遵循標準但特定於平臺的方法將庫名稱轉換為本機庫名稱。 例如,Solaris系統將名稱pkg_Cls
轉換為libpkg_Cls.so
,而Win32系統將相同的pkg_Cls
名稱轉換為pkg_Cls.dll
。
程式設計師可以使用單個庫來儲存任意數量的類所需的所有本機方法,只要這些類要使用相同的類載入器載入即可。 VM在內部維護每個類載入器的載入本機庫列表。 供應商應選擇本地庫名稱,以儘量減少名稱衝突的可能性。
本機庫可以與VM 靜態連結 。 組合庫和VM映像的方式取決於實現。 System.loadLibrary
或等效的API必須成功才能將此庫視為已載入。
當且僅當庫匯出名為JNI_OnLoad_L
的函式時,其影象已與VM組合的庫L被定義為靜態連結。
如果靜態連結庫L匯出名為JNI_OnLoad_L
的函式和名為JNI_OnLoad
的函式,則將忽略JNI_OnLoad
函式。
如果庫L是靜態連結的,那麼在第一次呼叫System.loadLibrary("L")
或等效的API時,將使用為JNI_OnLoad function
指定的相同引數和預期返回值呼叫JNI_OnLoad function
。
靜態連結的庫L將禁止動態載入同名庫。
當包含靜態連結的本機庫L的類載入器被垃圾收集時,如果匯出這樣的函式,VM將呼叫庫的JNI_OnUnload_L
函式。
如果靜態連結庫L匯出名為JNI_OnUnLoad_L
的函式和名為JNI_OnUnLoad
的函式,則將忽略JNI_OnUnLoad
函式。
程式設計師還可以呼叫JNI函式RegisterNatives()
來註冊與類關聯的本機方法。 RegisterNatives()
函式對於靜態連結函式特別有用。
動態連結器根據其名稱解析條目。 本機方法名稱由以下元件連線:
- 字首
Java_
- 一個錯位的完全限定的類名
- 下劃線(“_”)分隔符
- 一個受損的方法名稱
- 對於過載的本機方法,兩個下劃線(“__”)後跟受損的引數簽名
VM檢查駐留在本機庫中的方法的方法名稱匹配。 VM首先查詢短名稱; 也就是說,沒有引數簽名的名稱。 然後它查詢長名稱,這是帶有引數簽名的名稱。 只有當本機方法使用另一個本機方法過載時,程式設計師才需要使用長名稱。 但是,如果本機方法與非本地方法具有相同的名稱,則這不是問題。 非本地方法(Java方法)不駐留在本機庫中。
在以下示例中,本機方法g
不必使用長名稱進行連結,因為其他方法g
不是本機方法,因此不在本機庫中。
class Cls1 {
int g(int i);
native int g(double d);
}
我們採用了一種簡單的名稱修改方案,以確保所有Unicode字元都轉換為有效的C函式名稱。 我們使用下劃線(“_”)字元代替完全限定類名中的斜槓(“/”)。 由於名稱或型別描述符永遠不會以數字開頭,因此我們可以使用_0
,..., _9
作為轉義序列,如下表所示:
逃脫序列 | 表示 |
---|---|
_0XXXX |
Unicode字元XXXX 。 請注意,小寫字母用於表示非ASCII Unicode字元,例如_0abcd 而不是_0ABCD 。 |
_1 |
人物 ”_” |
_2 |
簽名中的字元“;” |
_3 |
簽名中的字元“[” |
本機方法和介面API都遵循給定平臺上的標準庫呼叫約定。 例如,UNIX系統使用C呼叫約定,而Win32系統使用__stdcall。
JNI介面指標是本機方法的第一個引數。 JNI介面指標的型別為JNIEnv 。 第二個引數根據本機方法是靜態方法還是非靜態方法而有所不同。 非靜態本機方法的第二個引數是對該物件的引用。 靜態本機方法的第二個引數是對其Java類的引用。
其餘引數對應於常規Java方法引數。 本機方法呼叫通過返回值將其結果傳遞迴呼叫例程。 第3章:JNI型別和資料結構描述了Java和C型別之間的對映。
以下程式碼示例說明如何使用C函式實現本機方法f
。 本機方法f
宣告如下:
package pkg;
class Cls {
native double f(int i, String s);
// ...
}
具有長錯位名稱Java_pkg_Cls_f_ILjava_lang_String_2
的C函式實現本機方法f
:
jdouble Java_pkg_Cls_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”/ *指定C呼叫約定* /
jdouble Java_pkg_Cls_f__ILjava_lang_String_2(
JNIEnv * env,/ *介面指標* /
jobject obj,/ *“this”指標* /
jint i,/ *引數#1 * /
jstring s)/ *引數#2 * /
{
const char * str = env-> GetStringUTFChars(s,0);
// ...
env-> ReleaseStringUTFChars(s,str);
//返回...
}
使用C ++,額外的間接級別和介面指標引數從原始碼中消失。 但是,底層機制與C完全相同。在C ++中,JNI函式被定義為內聯成員函式,它們擴充套件為C對應函式。
原始型別(如整數,字元等)在Java和本機程式碼之間複製。 另一方面,任意Java物件都通過引用傳遞。 VM必須跟蹤已傳遞給本機程式碼的所有物件,以便垃圾收集器不會釋放這些物件。 反過來,本機程式碼必須有一種方法來通知VM它不再需要這些物件。 此外,垃圾收集器必須能夠移動本機程式碼引用的物件。
JNI將本機程式碼使用的物件引用分為兩類: 本地 引用和全域性引用 。 本地引用在本機方法呼叫的持續時間內有效,並在本機方法返回後自動釋放。 全域性引用在顯式釋放之前仍然有效。
物件作為本地引用傳遞給本機方法。 JNI函式返回的所有Java物件都是本地引用。 JNI允許程式設計師從本地引用建立全域性引用。 JNI函式,期望Java物件接受全域性和本地引用。 本機方法可以返回對VM的本地或全域性引用作為其結果。
在大多數情況下,程式設計師應該依賴VM在本機方法返回後釋放所有本地引用。 但是,有時程式設計師應該明確地釋放本地引用。 例如,考慮以下情況:
- 本機方法訪問大型Java物件,從而建立對Java物件的本地引用。 然後,本機方法在返回呼叫方之前執行其他計算。 對大型Java物件的本地引用將阻止物件被垃圾回收,即使該物件不再用於計算的其餘部分。
- 本機方法會建立大量本地引用,但並非所有引用都同時使用。 由於VM需要一定的空間來跟蹤本地引用,因此建立太多本地引用可能會導致系統記憶體不足。 例如,本機方法迴圈遍歷大量物件,將元素作為本地引用檢索,並在每次迭代時對一個元素進行操作。 在每次迭代之後,程式設計師不再需要對陣列元素的本地引用。
JNI允許程式設計師在本機方法中的任何點手動刪除本地引用。 為了確保程式設計師可以手動釋放本地引用,不允許JNI函式建立額外的本地引用,除了它們作為結果返回的引用。
本地引用僅在建立它們的執行緒中有效。 本機程式碼不能將本地引用從一個執行緒傳遞到另一個執行緒。
為了實現本地引用,Java VM為從Java到本機方法的每次控制轉換建立了一個登錄檔。 登錄檔將不可移動的本地引用對映到Java物件,並防止物件被垃圾回收。 傳遞給本機方法的所有Java物件(包括那些作為JNI函式呼叫結果返回的物件)都會自動新增到登錄檔中。 在本機方法返回後刪除登錄檔,允許其所有條目被垃圾回收。
有不同的方法來實現登錄檔,例如使用表,連結串列或雜湊表。 雖然引用計數可用於避免登錄檔中的重複條目,但JNI實現沒有義務檢測和摺疊重複條目。
請注意,通過保守掃描本機堆疊,無法忠實地實現本地引用。 本機程式碼可以將本地引用儲存到全域性或堆資料結構中。
JNI在全域性和本地引用上提供了豐富的訪問器函式。 這意味著無論VM如何在內部表示Java物件,相同的本機方法實現都會起作用。 這是JNI可以被各種VM實現支援的關鍵原因。
通過不透明引用使用訪問器函式的開銷高於直接訪問C資料結構的開銷。 我們相信,在大多數情況下,Java程式設計師使用本機方法來執行非常重要的任務,這些任務會掩蓋此介面的開銷。
對於包含許多基本資料型別的大型Java物件(例如整數陣列和字串),此開銷是不可接受的。 (考慮用於執行向量和矩陣計算的本機方法。)迭代Java陣列並使用函式呼叫檢索每個元素是非常低效的。
一種解決方案引入了“固定”的概念,以便本機方法可以要求VM確定陣列的內容。 然後,本機方法接收指向元素的直接指標。 然而,這種方法有兩個含義:
- 垃圾收集器必須支援固定。
- VM必須在記憶體中連續佈局原始陣列。 雖然這是大多數原始陣列最自然的實現,但是布林陣列可以實現為打包或解包。 因此,依賴於布林陣列的確切佈局的本機程式碼將不可移植。
我們採取妥協方案,克服上述兩個問題。
首先,我們提供了一組函式來複制Java陣列的一段和本機記憶體緩衝區之間的原始陣列元素。 如果本機方法只需要訪問大型陣列中的少量元素,請使用這些函式。
其次,程式設計師可以使用另一組函式來檢索陣列元素的固定版本。 請記住,這些功能可能需要Java VM執行儲存分配和複製。 這些函式實際上是否複製陣列取決於VM實現,如下所示:
- 如果垃圾收集器支援固定,並且陣列的佈局與本機方法的預期相同,則不需要複製。
- 否則,將陣列複製到不可移動的記憶體塊(例如,在C堆中)並執行必要的格式轉換。 返回指向副本的指標。
最後,該介面提供了通知VM本機程式碼不再需要訪問陣列元素的功能。 當您呼叫這些函式時,系統會取消陣列,或者將原始陣列與其不可移動的副本進行協調並釋放副本。
我們的方法提供靈活性 垃圾收集器演算法可以針對每個給定陣列單獨決定複製或固定。 例如,垃圾收集器可以複製小物件,但可以固定較大的物件。
JNI實現必須確保在多個執行緒中執行的本機方法可以同時訪問同一個陣列。 例如,JNI可以為每個固定陣列保留一個內部計數器,這樣一個執行緒就不會取消固定另一個執行緒固定的陣列。 請注意,JNI不需要鎖定原始陣列以供本機方法獨佔訪問。 同時從不同的執行緒更新Java陣列會導致不確定的結果。
JNI允許本機程式碼訪問欄位並呼叫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不會阻止VM解除安裝已從中派生ID的類。 解除安裝類後,方法或欄位ID將變為無效。 因此,本機程式碼必須確保:
- 保持對基礎類的實時引用,或
- 重新計算方法或欄位ID
如果它打算長時間使用方法或欄位ID。
JNI不對內部如何實現欄位和方法ID施加任何限制。
JNI不檢查程式設計錯誤,例如傳入NULL指標或非法引數型別。 非法引數型別包括使用普通Java物件而不是Java類物件。 由於以下原因,JNI不檢查這些程式設計錯誤:
- 強制JNI函式檢查所有可能的錯誤條件會降低正常(正確)本機方法的效能。
- 在許多情況下,沒有足夠的執行時型別資訊來執行此類檢查。
大多數C庫函式都不能防止程式設計錯誤。 例如, printf()
函式在收到無效地址時通常會導致執行時錯誤,而不是返回錯誤程式碼。 強制C庫函式檢查所有可能的錯誤條件可能會導致重複此類檢查 - 一次在使用者程式碼中,然後再次在庫中。
程式設計師不得將非法指標或錯誤型別的引數傳遞給JNI函式。 這樣做可能會導致任意後果,包括系統狀態損壞或VM崩潰。
JNI允許本機方法引發任意Java異常。 本機程式碼也可以處理未完成的Java異常。 未處理的Java異常會傳播回VM。
某些JNI函式使用Java異常機制來報告錯誤情況。 在大多數情況下,JNI函式通過返回錯誤程式碼並丟擲Java異常來報告錯誤情況。 錯誤程式碼通常是一個特殊的返回值(如NULL),它超出了正常返回值的範圍。 因此,程式設計師可以:
- 快速檢查上次JNI呼叫的返回值,以確定是否發生了錯誤,並且
- 呼叫一個函式
ExceptionOccurred()
,以獲取包含錯誤條件的更詳細描述的異常物件。
在兩種情況下,程式設計師需要檢查異常而無法首先檢查錯誤程式碼:
- 呼叫Java方法的JNI函式返回Java方法的結果。 程式設計師必須呼叫
ExceptionOccurred()
來檢查在執行Java方法期間可能發生的異常。 - 某些JNI陣列訪問函式不返回錯誤程式碼,但可能丟擲
ArrayIndexOutOfBoundsException
或ArrayStoreException
。
在所有其他情況下,非錯誤返回值可確保不會丟擲任何異常。
非同步異常
在多執行緒的情況下,除當前執行緒之外的執行緒可能釋出非同步異常。 非同步異常不會立即影響當前執行緒中本機程式碼的執行,直到:
- 本機程式碼呼叫可能引發同步異常的JNI函式之一,或
- 本機程式碼使用
ExceptionOccurred()
顯式檢查同步和非同步異常。
請注意,只有那些可能引發同步異常的JNI函式才會檢查非同步異常。
本機方法應在必要的位置插入ExceptionOccurred()
檢查(例如在沒有其他異常檢查的緊密迴圈中),以確保當前執行緒在合理的時間內響應非同步異常。
異常處理
有兩種方法可以處理本機程式碼中的異常:
- 本機方法可以選擇立即返回,從而導致在啟動本機方法呼叫的Java程式碼中丟擲異常。
- 本機程式碼可以通過呼叫
ExceptionClear()
來清除異常,然後執行自己的異常處理程式碼。
引發異常後,本機程式碼必須首先清除異常,然後再進行其他JNI呼叫。 當存在掛起的異常時,可以安全呼叫的JNI函式是:
ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()