天天看點

《Java 本地接口規範》- 設計概述 設計概述 JNI 接口函數和指針 加載和連結本地方法 引用 Java 對象 通路 Java 對象 報告程式設計錯誤 Java 異常

本章着重讨論 jni 中的主要設計問題,其中的大部分問題都與本地方法有關。調用 api 的設計将在

第 5 章 “調用 api” 中讨論。

平台相關代碼是通過調用 jni 函數來通路 java 虛拟機功能的。jni 函數可通過接口指針來獲得。接口指針是指針的指針,它指向一個指針數組,而指針數組中的每個元素又指向一個接口函數。每個接口函數都處在數組的某個預定偏移量中。圖 2-1 說明了接口指針的組織結構。

《Java 本地接口規範》- 設計概述 設計概述 JNI 接口函數和指針 加載和連結本地方法 引用 Java 對象 通路 Java 對象 報告程式設計錯誤 Java 異常

jni 接口的組織類似于 c++ 虛拟函數表或 com 接口。使用接口表而不使用硬性編入的函數表的好處是使 jni 名字空間與平台相關代碼分開。虛拟機可以很容易地提供多個版本的 jni 函數表。例如,虛拟機可支援以下兩個 jni 函數表:

一個表對非法參數進行全面檢查,适用于調試程式;

另一個表隻進行 jni 規範所要求的最小程度的檢查,是以效率較高。

jni 接口指針隻在目前線程中有效。是以,本地方法不能将接口指針從一個線程傳遞到另一個線程中。實作 jni 的虛拟機可将本地線程的資料配置設定和儲存在 jni 接口指針所指向的區域中。

本地方法将jni 接口指針當作參數來接受。虛拟機在從相同的 java 線程中對本地方法進行多次調用時,保證傳遞給該本地方法的接口指針是相同的。但是,一個本地方法可被不同的 java 線程所調用,是以可以接受不同的 jni 接口指針。

對本地方法的加載通過 <code>system.loadlibrary</code> 方法實作。下例中,類初始化方法加載了一個與平台有關的本地庫,在該本地庫中給出了本地方法<code>f</code> 的定義:

system.loadlibrary 的參數是程式員任意選取的庫名。系統按照标準的但與平台有關的處理方法将該庫名轉換為本地庫名。例如,solaris 系統将名稱<code>pkg_cls</code> 轉換為<code>libpkg_cls.so</code>,而 win32 系統将相同的名稱

<code>pkg_cls</code> 轉換為<code>pkg_cls.dll</code>。

程式員可用單個庫來存放任意數量的類所需的所有本地方法,隻要這些類将被相同的類加載器所加載。虛拟機在其内部為每個類加載器保護其所加載的本地庫清單。提供者應該盡量選擇能夠避免名稱沖突的本地庫名。

如果底層作業系統不支援動态連結,則必須事先将所有的本地方法連結到虛拟機上。這種情況下,虛拟機實際上不需要加載庫即可完成<code>system.loadlibrary</code> 調用。

程式員還可調用 jni 函數 <code>registernatives()</code> 來注冊與類關聯的本地方法。在與靜态連結的函數一起使用時,<code>registernatives()</code> 函數将特别有用。

動态連結程式是根據項的名稱來解析各項的。本地方法名由以下幾部分串接而成:

字首 <code>java_</code>

mangled 全限定的類名

下劃線(“_”)分隔符

mangled 方法名

對于重載的本地方法,加上兩個下劃線(“__”),後跟 mangled 參數簽名

虛拟機将為本地庫中的方法查找比對的方法名。它首先查找短名(沒有參數簽名的名稱),然後再查找帶參數簽名的長名稱。隻有當某個本地方法被另一個本地方法重載時程式員才有必要使用長名。但如果本地方法的名稱與非本地方法的名稱相同,則不會有問題。因為非本地方法(java 方法)并不放在本地庫中。

下例中,不必用長名來連結本地方法 <code>g</code>,因為另一個方法<code>g</code> 不是本地方法,因而它并不在本地庫中。

我們采取簡單的名字攪亂方案,以保證所有的 unicode 字元都能被轉換為有效的 c 函數名。我們用下劃線(“_”) 字元來代替全限定的類名中的斜杠(“/”)。由于名稱或類型描述符從來不會以數字打頭,我們用<code>_0</code>、...、<code>_9</code>來代替轉義字元序列,如表

2-1所示:

轉義字元序列

表示

<code>_0xxxx</code>

unicode 字元<code>xxxx</code>。

<code>_1</code>

字元“_”

<code>_2</code>

簽名中的字元“;”

<code>_3</code>

簽名中的字元“[”

本地方法和接口 api 都要遵守給定平台上的庫調用标準約定。例如,unix 系統使用 c 調用約定,而 win32 系統使用 __stdcall。

jni 接口指針是本地方法的第一個參數。其類型是 jnienv。第二個參數随本地方法是靜态還是非靜态而有所不同。非靜态本地方法的第二個參數是對對象的引用,而靜态本地方法的第二個參數是對其 java 類的引用。

其餘的參數對應于通常 java 方法的參數。本地方法調用利用傳回值将結果傳回調用程式中。第 3 章 “jni 的類型和資料結構” 将描述 java 類型和 c 類型之間的映射。

代碼示例 2-1 說明了如何用 c 函數來實作本地方法<code>f</code>。對本地方法<code>f</code> 的聲明如下:

具有長 mangled 名稱<code>java_pkg_cls_f_iljava_lang_string_2</code> 的 c 函數實作本地方法<code>f</code>:

注意,我們總是用接口指針 env 來操作 java 對象。可用 c++ 将此代碼寫得稍微簡潔一些,如代碼示例 2-2 所示:

使用 c++ 後,源代碼變得更為直接,且接口指針參數消失。但是,c++ 的内在機制與 c 的完全一樣。在 c++ 中,jni 函數被定義為内聯成員函數,它們将擴充為相應的 c 對應函數。

基本類型(如整型、字元型等)在 java 和平台相關代碼之間直接進行複制。而 java 對象由引用來傳遞。虛拟機必須跟蹤傳到平台相關代碼中的對象,以使這些對象不會被垃圾收集器釋放。反之,平台相關代碼必須能用某種方式通知虛拟機它不再需要那些對象,同時,垃圾收集器必須能夠移走被平台相關代碼引用過的對象。

jni 将平台相關代碼使用的對象引用分成兩類:局部引用和全局引用。局部引用在本地方法調用期間有效,并在本地方法傳回後被自動釋放掉。全局引用将一直有效,直到被顯式釋放。

對象是被作為局部引用傳遞給本地方法的,由 jni 函數傳回的所有 java 對象也都是局部引用。jni 允許程式員從局部引用建立全局引用。要求 java 對象的 jni 函數既可接受全局引用也可接受局部引用。本地方法将局部引用或全局引用作為結果傳回。

大多數情況下,程式員應該依靠虛拟機在本地方法傳回後釋放所有局部引用。但是,有時程式員必須顯式釋放某個局部引用。例如,考慮以下的情形:

本地方法要通路一個大型 java 對象,于是建立了對該 java 對象的局部引用。然後,本地方法要在傳回調用程式之前執行其它計算。對這個大型 java 對象的局部引用将防止該對象被當作垃圾收集,即使在剩餘的運算中并不再需要該對象。

本地方法建立了大量的局部引用,但這些局部引用并不是要同時使用。由于虛拟機需要一定的空間來跟蹤每個局部引用,建立太多的局部引用将可能使系統耗盡記憶體。例如,本地方法要在一個大型對象數組中循環,把取回的元素作為局部引用,并在每次疊代時對一個元素進行操作。每次疊代後,程式員不再需要對該數組元素的局部引用。

jni 允許程式員在本地方法内的任何地方對局部引用進行手工删除。為確定程式員可以手工釋放局部引用,jni 函數将不能建立額外的局部引用,除非是這些 jni 函數要作為結果傳回的引用。

局部引用僅在建立它們的線程中有效。本地方法不能将局部引用從一個線程傳遞到另一個線程中。

為了實作局部引用,java 虛拟機為每個從 java 到本地方法的控制轉換都建立了注冊服務程式。注冊服務程式将不可移動的局部引用映射為 java 對象,并防止這些對象被當作垃圾收集。所有傳給本地方法的 java 對象(包括那些作為 jni 函數調用結果傳回的對象)将被自動添加到注冊服務程式中。本地方法傳回後,注冊服務程式将被删除,其中的所有項都可以被當作垃圾來收集。

可用各種不同的方法來實作注冊服務程式,例如,使用表、連結清單或 hash 表來實作。雖然引用計數可用來避免注冊服務程式中有重複的項,但 jni 實作不是必須檢測和消除重複的項。

注意,以保守方式掃描本地堆棧并不能如實地實作局部引用。平台相關代碼可将局部引用儲存在全局或堆資料結構中。

jni 提供了一大批用來通路全局引用和局部引用的函數。這意味着無論虛拟機在内部如何表示 java 對象,相同的本地方法實作都能工作。這就是為什麼 jni 可被各種各樣的虛拟機實作所支援的關鍵原因。

通過不透明的引用來使用通路函數的開銷比直接通路 c 資料結構的開銷來得高。我們相信,大多數情況下,java 程式員使用本地方法是為了完成一些重要任務,此時這種接口的開銷不是首要問題。

對于含有大量基本資料類型(如整數數組和字元串)的 java 對象來說,這種開銷将高得不可接受 (考慮一下用于執行矢量和矩陣運算的本地方法的情形便知)。對 java 數組進行疊代并且要通過函數調用取回數組的每個元素,其效率是非常低的。

一個解決辦法是引入“釘住”概念,以使本地方法能夠要求虛拟機釘住數組内容。而後,該本地方法将接受指向數值元素的直接指針。但是,這種方法包含以下兩個前提:

垃圾收集器必須支援釘住。

虛拟機必須在記憶體中連續存放基本類型數組。雖然大多數基本類型數組都是連續存放的,但布爾數組可以壓縮或不壓縮存儲。是以,依賴于布爾數組确切存儲方式的本地方法将是不可移植的。

我們将采取折衷方法來克服上述兩個問題。

首先,我們提供了一套函數,用于在 java 數組的一部分和本地記憶體緩沖之間複制基本類型數組元素。這些函數隻有在本地方法隻需通路大型數組中的一小部分元素時才使用。

其次,程式員可用另一套函數來取回數組元素的受限制版本。記住,這些函數可能要求 java 虛拟機配置設定存儲空間和進行複制。虛拟機實作将決定這些函數是否真正複制該數組,如下所示:

如果垃圾收集器支援釘住,且數組的布局符合本地方法的要求,則不需要進行複制。

否則,該數組将被複制到不可移動的記憶體塊中(例如,複制到 c 堆中),并進行必要的格式轉換,然後傳回指向該副本的指針。

最後,接口提供了一些函數,用以通知虛拟機本地方法已不再需要通路這些數組元素。當調用這些函數時,系統或者釋放數組,或者在原始數組與其不可移動副本之間進行協調并将副本釋放。

這種處理方法具有靈活性。垃圾收集器的算法可對每個給定的數組分别作出複制或釘住的決定。例如,垃圾收集器可能複制小型對象而釘住大型對象。

jni 實作必須確定多個線程中運作的本地方法可同時通路同一數組。例如,jni 可以為每個被釘住的數組保留一個内部計數器,以便某個線程不會解開同時被另一個線程釘住的數組。注意,jni 不必将基本類型數組鎖住以專供某個本地方法通路。同時從不同的線程對 java 數組進行更新将導緻不确定的結果。

jni 允許本地方法通路 java 對象的域或調用其方法。jni 用符号名稱和類型簽名來識别方法和域。從名稱和簽名來定位域或對象的過程可分為兩步。例如,為調用類cls 中的<code>f</code> 方法,平台相關代碼首先要獲得方法 id,如下所示:

然後,平台相關代碼可重複使用該方法 id 而無須再查找該方法,如下所示:

域 id 或方法 id 并不能防止虛拟機解除安裝生成該 id 的類。該類被解除安裝之後,該方法 id 或域 id 亦變成無效。是以,如果平台相關代碼要長時間使用某個方法 id 或域 id,則它必須確定:

保留對所涉及類的活引用,或

重新計算該方法 id 或域 id。

jni 對域 id 和方法 id 的内部實作并不施加任何限制。

jni 不檢查諸如傳遞 null 指針或非法參數類型之類的程式設計錯誤。非法的參數類型包括諸如要用 java 類對象時卻用了普通 java 對象這樣的錯誤。jni 不檢查這些程式設計錯誤的理由如下:

強迫 jni 函數去檢查所有可能的錯誤情況将降低正常(正确)的本地方法的性能。

在許多情況下,沒有足夠的運作時的類型資訊可供這種檢查使用。

大多數 c 庫函數對程式設計錯誤不進行防範。例如,<code>printf()</code> 函數在接到一個無效位址時通常是引起運作錯而不是傳回錯誤代碼。強迫 c 庫函數檢查所有可能的錯誤情況将有可能引起這種檢查被重複進行--先是在使用者代碼中進行,然後又在庫函數中再次進行。

程式員不得将非法指針或錯誤類型的參數傳遞給 jni 函數。否則,可能産生意想不到的後果,包括可能使系統狀态受損或使虛拟機崩潰。

jni 允許本地方法抛出任何 java 異常。本地方法也可以處理突出的 java 異常。未被處理的 java 異常将被傳回虛拟機中。

一些 jni 函數使用 java 異常機制來報告錯誤情況。大多數情況下,jni 函數通過傳回錯誤代碼并抛出 java 異常來報告錯誤情況。錯誤代碼通常是特殊的傳回值(如 null),這種特殊的傳回值在正常傳回值範圍之外。是以,程式員可以:

快速檢查上一個 jni 調用所傳回的值以确定是否出錯,并

通過調用函數 <code>exceptionoccurred()</code> 來獲得異常對象,它含有對錯誤情況的更詳細說明。

在以下兩種情況中,程式員需要先查出異常,然後才能檢查錯誤代碼:

調用 java 方法的 jni 函數傳回該 java 方法的結果。程式員必須調用 <code>exceptionoccurred()</code> 以檢查在執行 java 方法期間可能發生的異常。

某些用于通路 jni 數組的函數并不傳回錯誤代碼,但可能會抛出 <code>arrayindexoutofboundsexception</code> 或<code>arraystoreexception</code>。

在所有其它情況下,傳回值如果不是錯誤代碼值就可確定沒有抛出異常。

在多個線程的情況下,目前線程以外的其它線程可能會抛出異步異常。異步異常并不立即影響目前線程中平台相關代碼的執行,直到出現下列情況:

該平台相關代碼調用某個有可能抛出同步異常的 jni 函數,或者

該平台相關代碼用 <code>exceptionoccurred()</code> 顯式檢查同步異常或異步異常。

注意,隻有那些有可能抛出同步異常的 jni 函數才檢查異步異常。

本地方法應在必要的地方(例如,在一個沒有其它異常檢查的緊密循環中)插入 <code>exceptionoccurred()</code> 檢查以確定目前線程可在适當時間内對異步異常作出響應。

可用兩種方法來處理平台相關代碼中的異常:

本地方法可選擇立即傳回,使異常在啟動該本地方法調用的 java 代碼中抛出。

平台相關代碼可通過調用 <code>exceptionclear()</code> 來清除異常,然後執行自己的異常處理代碼。

抛出了某個異常之後,平台相關代碼必須先清除異常,然後才能進行其它的 jni 調用。當有待定異常時,隻有以下這些 jni 函數可被安全地調用:<code>exceptionoccurred()、exceptiondescribe()</code> 和<code>exceptionclear()</code>。<code>exceptiondescribe()</code> 函數将列印有關待定異常的調試消息。