JNI Design Overview

翻譯自 https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html

Chapter   2

本章重點介紹JNI中的主要設計問題。 本節中的大多數設計問題都與本地方法有關。 第5章將介紹調用API的相關設計。

 

JNI Interface Functions and Pointers

本地代碼通過調用JNI函數來訪問Java VM功能。 JNI函數可通過接口指針使用。 接口指針是指向指針的指針。 該指針指向一個指針數組,每個指針指向一個接口函數。 每個接口函數都在數組內的預定義偏移處。 圖2-1展示了接口指針的組織結構。

The previous context describes this image.

 Figure 2-1 Interface Pointer

JNI接口的組織方式類似於C ++虛函數表或COM接口。 使用接口表而不是固定函數集的優點是,JNI名稱空間與本地代碼分離。 VM可以輕鬆提供多個版本的JNI功能表。 例如,VM可能支持兩個JNI功能表:

  • 一個執行徹底的非法參數檢查,並且適合調試;
  •      另一個執行JNI規範所需的最少檢查,因此效率更高。

JNI接口指針僅在當前線程中有效。 因此,本地方法不得將接口指針從一個線程傳遞到另一個線程。 實現JNI的VM可以在JNI接口指針所指向的區域中分配和存儲線程局部數據。

本地方法接收JNI接口指針作爲參數。當從同一Java線程對本地方法進行多次調用時, VM保證VM將相同的接口指針傳遞給本地方法。 無論如何,本地方法可以在不同的Java線程調用,因此可以接收不同的JNI接口指針。

Compiling, Loading and Linking Native Methods

由於Java VM是多線程的,因此本地方法庫也應被編譯,並與支持多線程的本地編譯器鏈接。 例如,對於使用Sun Studio編譯器編譯的C ++代碼,應使用-mt 標誌。 對於符合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調用。

程序員還可以調用JNI函數RegisterNatives() 來註冊與類關聯的本地方法。 RegisterNatives()函數對於靜態鏈接的函數特別有用。

Resolving Native Method Names

動態鏈接器根據名稱來進行解析。本地方法名稱由以下組件拼接而成:

  •     前綴Java_
  •     完整的類名
  •     下劃線(“ _”)分隔符
  •     對於重載的本地方法,兩個下劃線(“ __”)後跟參數簽名

VM檢查與本地庫中駐留的方法相匹配的方法名稱。 VM首先尋找簡稱;即沒有參數簽名的名稱。然後,它將查找長名稱,即帶有參數簽名的名稱。僅當本地方法被另一個本地方法重載時,程序員才需要使用長名稱。無論如何,如果本地方法與java方法具有相同的名稱,則這不是問題。非本機方法(Java方法)不駐留在本機庫中。

在下面的示例中,不必使用長名稱鏈接本駐留方法g,因爲另一個方法g不是本地方法,因此不在本地庫中。

class Cls1 { 

  int g(int i); 

  native int g(double d); 

} 

 

我們採用了一種簡單的名稱處理方案,以確保所有Unicode字符都轉換爲有效的C函數名稱。 在完全限定的類名稱中,我們使用下劃線(_)代替斜槓(“ /”)。 由於名稱或類型描述符從不以數字開頭,因此我們可以將_0,...,_ 9用於轉義序列,如表2-1所示:

Table 2-1 Unicode Character Translation

Escape Sequence

Denotes

_0XXXX

a Unicode character XXXX.
Note that lower case is used
to represent non-ASCII
Unicode characters, e.g.,
_0abcd as opposed to
_0ABCD.

_1

the character “_”

_2

the character “;” in signatures

_3

the character “[“ in signatures

 

本地方法和接口API都在給定平臺上遵循標準的庫調用約定。 例如,UNIX系統使用C調用約定,而Win32系統使用__stdcall。

Native Method Arguments

 

JNI接口指針是本地方法的第一個參數。 JNI接口指針的類型爲JNIEnv。 第二個參數根據本地方法是靜態方法還是非靜態方法而有所不同。 非靜態本地方法的第二個參數是對對象的引用。 靜態本地方法的第二個參數是對其Java類的引用。

其餘參數對應於常規Java方法參數。 本地方法調用通過返回值將其結果傳遞迴調用例程。 第3章介紹Java和C類型之間的映射。

Example 2-1演示了使用C函數來實現本地方法f。 本地方法f聲明如下: 

package pkg;  

class Cls { 

     native double f(int i, String s); 

     ... 

} 

長整齊的名稱Java_pkg_Cls_f_ILjava_lang_String_2的C函數實現本地方法f:

Example 2-1 Implementing a Native Method Using C

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 ++,您可以編寫稍微乾淨一點的代碼版本,如Example 2-2所示:

Code Example 2-2 Implementing a Native Method Using C++

extern "C" /* specify the C calling convention */  

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 */ 

{ 

     const char *str = env->GetStringUTFChars(s, 0); 

     ... 

     env->ReleaseStringUTFChars(s, str); 

     return ... 

} 

使用C ++時,額外的間接級別和接口指針參數將從源代碼中消失。 但是,底層機制與C完全相同。在C ++中,JNI函數被定義爲擴展爲C對應函數的內聯成員函數。

Referencing Java Objects

在Java和本地代碼之間互相拷貝基本類型,例如整型,字符型等。 另一方面,Java對象通過引用傳遞。 VM必須追蹤已傳遞給本地代碼的所有對象,以使垃圾回收器不會回收這些對象。 反過來,本地代碼不再需要java對象時,必須有方法來通知VM。 另外,垃圾收集器必須能夠移除被本地代碼引用的對象。

Global and Local References

JNI將本機代碼使用的對象引用分爲兩類:局部引用和全局引用。 局部引用在本地方法調用期間有效,並在本地方法返回後自動釋放。 全局引用在顯式釋放之前一直保持有效。

對象以局部引用的形式傳遞給本地方法。 JNI函數返回的所有Java對象都是局部引用。 JNI允許程序員從局部引用創建全局引用。 期望Java對象的JNI函數接受全局和局部引用。 本地方法可能會返回VM的局部或全局引用作爲其結果。

在大多數情況下,程序員應在本地方法返回後依靠VM回收所有局部引用。 但是,有時程序員應該顯式釋放局部引用。 例如,考慮以下情況:

  • 本地方法訪問一個大的Java對象,並創建該Java對象的局部引用。 然後,本地方法將執行其他計算,然後再返回到調用方。 即使在其餘的計算中不再使用該對象,該Java對象的局部引用也防止了GC對該對象進行垃圾回收。
  • 本地方法創建了大量局部引用,儘管並非所有的局部引用都會被使用。 由於VM需要一定的空間來跟蹤局部引用,因此創建太多局部引用可能會導致系統內存不足。 例如,本地方法遍歷一個大容量的對象數組,檢索元素作爲局部引用,並在每次迭代時對一個元素進行操作。 每次迭代後,程序員不再需要這個數組元素的本地引用。

JNI允許程序員在本地方法中的任何時候手動刪除局部引用。 爲了確保程序員可以手動釋放局部引用,不允許JNI函數創建額外的局部引用,除非它們會作爲結果返回引用。

局部引用僅在創建它們的線程中有效。 本地代碼不得將局部引用從一個線程傳遞到另一個線程。

Implementing Local References

爲了實現局部引用,Java VM爲從Java到本地方法的每次控制轉換創建一個註冊表。 註冊表將不可刪除的局部引用映射到Java對象,並防止垃圾回收對象。 傳遞給本地方法的所有Java對象(包括那些作爲JNI函數調用結果返回的Java對象)都將自動添加到註冊表中。 本地方法返回後,註冊表將被刪除,註冊表中的所有子項都允許被GC垃圾回收。

有多種實現註冊表的方法,例如使用表,鏈表或哈希表。 儘管可以使用引用計數來避免註冊表中出現重複項,但是JNI實現沒有義務檢測並刪除重複項。

請注意,不能通過保守地掃描本地堆棧來實現局部引用。 本地代碼可以將局部引用存儲到全局或堆數據結構中。

Accessing Java Objects

JNI爲全局和局部引用提供了一組功能豐富的訪問器。 無論VM在內部如何表示Java對象,都可以運行同一個本地方法。 這就是爲什麼JNI可以被各種VM支持的關鍵原因。

通過不透明引用使用訪問器函數的開銷比直接訪問C數據結構的開銷高。 我們認爲,在大多數情況下,Java程序員使用本地方法來執行重要的任務,從而使該接口的開銷變得微不足道。

Accessing Primitive Arrays

對於包含許多基本數據類型(例如整型數組和字符串)的大型Java對象,此開銷是不可接受的。 (考慮用於執行矢量和矩陣計算的本地方法。)遍歷Java數組並使用函數調用檢索每個元素的效率很低。

一種解決方案引入了“固定”(pinning)的概念,以便本地方法可以要求VM固定數組的內容。 然後,本地方法接收指向元素的直接指針。 但是,此方法有兩個含義:

  • 垃圾收集器必須支持固定(pinning)。
  •      VM必須在內存中連續分配基本類型的數組。 儘管對於大多數基本類型的數組來說,這是最自然的實現,但是布爾數組可以打包或拆包形式實現。 因此,依賴於布爾數組的確切分配的本地代碼將不可移植。

 

我們採取了可以解決上述兩個問題的折衷方案。

首先,我們提供了一組函數,用於在一段Java數組和本地內存緩衝區之間複製基本數組元素。 如果本地方法僅需要訪問大型數組中的少量元素,請使用這些函數。

其次,程序員可以使用另一組函數來獲取固定形式的數組元素。 請記住,這些功能可能需要Java VM執行存儲分配和複製。 實際上是否會複製數組取決於VM的實現,如下所示:

  • 如果垃圾收集器支持固定,並且數組的分配與本地方法所期望的相同,則不需要複製。
  • 否則,將數組複製到固定的內存塊中(例如,在C堆中),並執行必要的格式轉換。 返回指向副本的指針。

最後,當本地代碼不再需要訪問數組元素時,該接口提供了一些功能用於通知VM。 當您調用這些函數時,系統要麼取消固定數組,要麼協調原始數組與其固定的副本,然後釋放該副本。

我們的方法提供了靈活性。 垃圾收集器算法可以爲每個給定數組做出有關複製或固定的單獨決策。 例如,垃圾收集器可以複製小對象,固定較大的對象。

JNI實現必須確保在多個線程中運行的本地方法可以同時訪問同一數組。 例如,JNI可以爲每個固定的數組保留一個內部計數器,以使一個線程不會取消固定被另一個線程固定的數組。 注意,JNI不需要鎖定基本數組就可任意通過本地方法進行獨佔訪問。 同時從不同的線程更新Java數組會導致不確定的結果。

Accessing Fields and Methods

The JNI allows native code to access the fields and to call the methods of Java objects. The JNI identifies methods and fields by their symbolic names and type signatures. A two-step process factors out the cost of locating the field or method from its name and signature. For example, to call the method f in class cls, the native code first obtains a method ID, as follows:

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或方法ID不會阻止VM將依據獲取ID的類卸載。卸載類後,方法ID或字段ID就失效了。因此,如果打算長時間使用方法或字段ID,本機代碼必須確保:

  • 保持對基礎類的實時引用,或者
  • 重新計算方法或字段ID

JNI不對字段和方法ID的內部實現施加任何限制。

Reporting Programming Errors

JNI不檢查編程錯誤,例如傳入NULL指針或非法參數類型。 非法的參數類型包括諸如本來應該是Java類對象,卻使用普通的Java對象。 由於以下原因,JNI不檢查這些編程錯誤:

  • 強制JNI函數檢查所有可能的錯誤情況會降低普通的本地方法的性能。
  • 在許多情況下,沒有足夠的運行時類型信息來執行此類檢查。

大多數C庫函數都不能防止編程錯誤。 例如,當printf()函數接收到無效地址時,通常會導致運行時錯誤,而不是返回錯誤碼。 強制C庫函數檢查所有可能的錯誤情況可能會導致重複這些檢查(一次在用戶代碼中,然後在庫中)。

程序員不得將非法的指針或錯誤類型的參數傳遞給JNI函數。 這樣做可能會導致很嚴重的後果,包括損壞的系統狀態或VM崩潰。

Java Exceptions

JNI允許本地方法引發任意Java異常。 本機代碼還可以處理未解決的Java異常。 未處理的Java異常將傳遞迴VM。

Exceptions and Error Codes

某些JNI函數使用Java異常機制來報告錯誤。 在大多數情況下,JNI函數通過返回錯誤碼並引發Java異常來報告錯誤。 錯誤碼通常是超出了正常返回值的範圍的特殊的返回值(例如NULL)。 因此,程序員可以:

  • 快速檢查最後一個JNI調用的返回值,以確定是否發生錯誤,並且
  • 調用函數ExceptionOccurred()以獲取包含錯誤狀態的詳細描述的異常對象。

 

在兩種情況下,程序員不能首先檢查錯誤碼,而需要檢查異常:

  • 調用Java方法並返回Java方法的執行結果的JNI函數。 程序員必須調用ExceptionOccurred()來檢查在執行Java方法期間可能發生的異常。
  •      一些JNI數組訪問函數不會返回錯誤碼,但是可能會拋出ArrayIndexOutOfBoundsException或ArrayStoreException。

除此之外,非錯誤的返回值可確保沒有引發任何異常。

Asynchronous Exceptions(異步異常)

在有多線程的情況下,當前線程以外的其他線程可能會拋出異步異常。 異步異常不會立即影響當前線程中本地代碼的執行,直到:

  •      本地代碼調用了一個可能引發同步異常的JNI函數,或者
  •      本機代碼使用ExceptionOccurred()顯式檢查同步和異步異常。


請注意,只有那些可能引發同步異常的JNI函數纔會檢查異步異常。

本機方法應在必要的位置插入ExceptionOccurred()檢查(例如,在沒有其他異常檢查的循環調用中),以確保當前線程在合理的時間內響應異步異常。

Exception Handling

There are two ways to handle an exception in native code:

  • The native method can choose to return immediately, causing the exception to be thrown in the Java code that initiated the native method call.

  • The native code can clear the exception by calling ExceptionClear(), and then execute its own exception-handling code.

After an exception has been raised, the native code must first clear the exception before making other JNI calls. When there is a pending exception, the JNI functions that are safe to call are:

處理本地代碼中的異常有兩種方法:

  •      本地方法可以選擇立即返回,從而導致在調用本地方法的Java代碼中拋出異常。
  •      本地代碼可以通過調用ExceptionClear()清除異常,然後執行其自己的異常處理代碼。


引發異常後,本地代碼必須首先清除異常,然後再進行其他JNI調用。 當存在未處理的異常時,以下的JNI函數是可以安全調用的:

  ExceptionOccurred()
  ExceptionDescribe()
  ExceptionClear()
  ExceptionCheck()
  ReleaseStringChars()
  ReleaseStringUTFChars()
  ReleaseStringCritical()
  Release<Type>ArrayElements()
  ReleasePrimitiveArrayCritical()
  DeleteLocalRef()
  DeleteGlobalRef()
  DeleteWeakGlobalRef()
  MonitorExit()
  PushLocalFrame()
  PopLocalFrame()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章