Java JNI機制

java JNI機制

JNI概述及簡單例子介紹

系統環境代指本地操做系統環境,它有本身的本地庫和CPU指令集。本地程序(Native Applications)使用C/C++這樣的本地語言來編寫,被編譯成只能在本地系統環境下運行的二進制代碼,並和本地庫連接在一塊兒。本地程序和本地庫通常地會依賴於一個特定的本地系統環境。好比,一個系統下編譯出來的C程序不能在另外一個系統中運行。java

    JNI的強大特性使咱們在使用JAVA平臺的同時,還能夠重用原來的本地代碼。做爲虛擬機實現的一部分,JNI容許JAVA和本地代碼間的雙向交互。程序員

JNI能夠這樣與本地程序進行交互:瀏覽器

  1. 你可使用JNI來實現"本地方法"(native methods),並在JAVA程序中調用它們。
  2. JNI支持一個"調用接口"(invocation interface),它容許你把一個JVM嵌入到本地程序中。本地程序能夠連接一個實現了JVM的本地庫,而後使用"調用接口"執行JAVA語言編寫的軟件模塊。例如,一個用C語言寫的瀏覽器能夠在一個嵌入式JVM上面執行從網上下載下來的applets

使用JAVA程序調用C函數打印"Hello World!"。這個過程包含下面幾個步驟:安全

  1. 建立一個類(HelloWorld.java)聲明本地方法。
  2. 使用javac編譯源文件HollowWorld.java,產生HelloWorld.class。使用javah –jni來生成C頭文件(HelloWorld.h),這個頭文件裏面包含了本地方法的函數原型。
  3. C代碼寫函數原型的實現。
  4. C函數實現編譯成一個本地庫,建立Hello-World.dll或者libHello-World.so
  5. 使用java命令運行HelloWorld程序,類文件HelloWorld.class和本地庫(HelloWorld.dll或者libHelloWorld.so)在運行時被加載。

1.建立一個Java類,裏面包含着一個native的方法和加載庫的方法loadLibrary。HelloNative.java代碼以下:數據結構

    首先你們注意的是native方法,那個加載庫的到後面也起做用。native 關鍵字告訴編譯器(實際上是JVM)調用的是該方法在外部定義,這裏指的是C。app

2.運行javah,獲得包含該方法的C聲明頭文件.h 函數

3.根據頭文件,寫c實現本地方法 編碼

這裏簡單地實現這個sayHello方法以下:spa

4.生成dll共享庫,而後Java程序load庫,調用便可 操作系統

在Windows上,MinGW GCC 運行以下

gcc -m64  -Wl,--add-stdcall-alias -I"C:\Program Files\Java\jdk1.7.0_71\include" -I"C:\Program Files\Java\jdk1.7.0_71\include\include\win32" -shared -o HelloNative.dll HelloNative.c

-m64表示生成dll庫是64位的。而後運行 HelloNative:

java HelloNative

終於成功地能夠看到控制檯打印以下:

Hello,JNI

 

JNI設計概述

JNI最重要的設計目標就是在不一樣操做系統上的JVM之間提供二進制兼容,作到一個本地庫不須要從新編譯就能夠運行不一樣的系統的JVM上面。爲了達到這一點兒,JNI設計時不能關心JVM的內部實現,由於JVM的內部實現機制在不斷地變,而咱們必須保持JNI接口的穩定。JNI的第二個設計目標就是高效。咱們可能會看到,有時爲了知足第一個目標,可能須要犧牲一點兒效率,所以,咱們須要在平臺無關和效率之間作一些選擇。最後,JNI必須是一個完整的體系。它必須提供足夠多的JVM功能讓本地程序完成一些有用的任務。JNI不能只針對一款特定的JVM,而是要提供一系列標準的接口讓程序員能夠把他們的本地代碼庫加載到不一樣的JVM中去。有時,調用特定JVM下實現的接口能夠提供效率,但更多的狀況下,咱們須要用更通用的接口來解決問題。

加載本地庫

在JAVA程序能夠調用一個本地方法之間,JVM必須先加載一個包含這個本地方法的本地庫。

本地庫經過類加載器定位。類加載器在JVM中有不少用途,如,加載類文件、定義類和接口、提供命令空間機制、定位本地庫等。在這裏,咱們會假設你對類加載器的基本原理已經瞭解,咱們會直接講述加載器加載和連接類的技術細節。每個類或者接口都會與最初讀取它的class文件並建立類或接口對象的那個類加載器關聯起來。只有在名字和定義它們的類加載器都相同的狀況下,兩個類或者接口的類型纔會一致。下圖中類加載器L1和L2都定義了一個名字爲C的類。這兩個類並不相同,由於它們包含了兩個不一樣的f方法,f方法返回類型不一樣。

上圖中的虛線表達了類加載器之間的關係。一個類加載器必須請求其它類加載器爲它加載類或者接口。例如,L1和L2都委託系統類加載器來加載系統類java.lang.String。委託機制,容許不一樣的類加載器分離系統類。由於L1和L2都委託了系統類加載器來加載系統類,因此被系統類加載器加載的系統類能夠在L1和L2之間共享。這種思想很必要,由於若是程序或者系統代碼對java.lang.String有不一樣的理解的話,就會出現類型安全問題。

假設兩個C類都有一個方法f。VM使用"C_f"來定位兩個C.f方法的本地代碼實現。爲了確保類C被連接到了正確的本地函數,每個類加載器都會保存一個與本身相關聯的本地庫列表。

正是因爲每個類加載器都保存着一個本地庫列表,因此,只要是被這個類加載器加載的類,均可以使用這個本地庫中的本地方法。所以,程序員可使用一個單一的庫來存儲全部的本地方法。當類加載器被回收時,本地庫也會被JVM自動被unload。

本地庫經過System.loadLibrary方法來加載。下面的例子中,類Cls靜態初始化時加載了一個本地庫,f方法就是定義在這個庫中的。

JVM會根據當前系統環境的不一樣,把庫的名字轉換成相應的本地庫名字。例如,Solaris下,mypkg會被轉化成libmypkg.so,而Win32環境下,被轉化成mypkg.dll。

JVM在啓動的時候,會生成一個本地庫的目錄列表,這個列表的具體內容依賴於當前的系統環境,好比Win32下,這個列表中會包含Windows系統目錄、當前工做目錄、PATH環境變量裏面的目錄。

System.loadLibrary在加載相應本地庫失敗時,會拋出UnsatisfiedLinkError錯誤。若是相應的庫已經加載過,這個方法不作任何事情。若是底層操做系統不支持動態連接,那麼全部的本地方法必須被prelink到VM上,這樣的話,VM中調用System.loadLibrary時實際上沒有加載任何庫。

JVM內部爲每個類加載器都維護了一個已經加載的本地庫的列表。它經過三步來決定一個新加載的本地庫應該和哪一個類加載器關聯。

  1. 肯定System.loadLibrary的調用者。
  2. 肯定定義調用者的類。
  3. 肯定類的加載器。

下面的例子中,JVM會把本地庫foo和定義C的類加載器關聯起來。

VM中規定,一個JNI本地庫只能被一個類加載器加載。當一個JNI本地庫已經被第一個類加載器加載後,第二個類加載器再加載時,會報UnsatisfiedLinkError。這樣規定的目的是爲了確保基於類加載器的命令空間分隔機制在本地庫中一樣有效。若是不這樣的話,經過本地方法進行操做JVM時,很容易形成屬於不一樣類加載器的類和接口的混亂。

一旦JVM回收類加載器,與這個類加載器關聯的本地庫就會被unload。由於類指向它本身的加載器,因此,這意味着,VM也會被這個類unload。

VM會在第一次使用一個本地方法的時候連接它。假設調用了方法g,而在g的方法體中出現了對方法f的調用,那麼本地方法f就會被連接。VM不該該過早地連接本地方法,由於這時候實現這些本地方法的本地庫可能尚未被load,從而致使連接錯誤。

連接一個本地方法須要下面這幾個步驟:

  1. 肯定定義了本地方法的類的加載器。
  2. 在加載器所關聯的本地庫列表中搜索實現了本地方法的本地函數。
  3. 創建內部的數據結構,使對本地方法的調用可能直接定向到本地函數。
  4. VM經過下面這幾步,同本地方法的名字生成與之對應的本地函數的名字:
  5. 前綴"Java_"。
  6. 類的全名。
  7. 下劃線分隔符"_"。
  8. 方法名字。
  9. 有方法重載的狀況時,還會有兩個下劃線("__"),後面跟着參數描述符。

VM在類加載器關聯的本地庫中搜索符合指定名字的本地函數。對每個庫進行搜索時,VM會先搜索短名字(short name),即沒有參數描述符的名字。而後搜索長名字(long name),即有參數描述符的名字。當兩個本地方法重載時,程序員須要使用長名字來搜索。但若是一個本地方法和一個非本地方法重載時,就不會使用長名字。

JNI使用一種簡單的名字編碼協議來確保全部的Unicode字符都被轉化成可用的C函數名字。用下劃線("_")分隔類的全名中的各部分,取代原來的點(".")。

若是多個本地庫中都存在與一個編碼後的本地方法名字相匹配的本地函數,哪一個本地庫首先被加載,則它裏面的本地函數就與這個本地方法連接。若是沒有哪一個函數與給定的本地方法相匹配,則UnsatisfiedLinkError被拋出。

程序員還能夠調用JNI函數RegisterNatives來註冊與一個類關聯的本地方法。這個JNI函數對靜態連接函數很是有用。

調用參數

調用轉換決定了一個本地函數如何接收參數和返回結果。目前沒有一個標準,主要取決於編譯器和本地語言的不一樣。JNI要求同一個系統環境下,調用轉換機制必須相同。例如,JNI在UNIX下使用C調用轉換,而在Win32下使用stdcall調用轉換。若是程序員須要調用的函數遵循不一樣的調用轉換機制,那麼最好寫一個轉換層來解決這個問題。

JNIEnv是一個指向線程局部數據的接口指針,這個指針裏面包含了一個指向函數表的指針。在這個表中,每個函數位於一個預約義的位置上面。JNIEnv很像一個C++虛函數表或者Microsoft COM接口。

線程的局部JNIEnv接口指針

若是一個函數實現了一個本地方法,那麼這個函數的第一個參數就是一個JNIEnv接口指針。從同一個線程中調用的本地方法,傳入的JNIEnv指針是相同的。本地方法可能被不一樣的線程調用,這時,傳入的JNIEnv指針是不一樣的。但JNIEnv間接指向的函數表在多個線程間是共享的。JNI指針指向一個線程內的局部數據結構是由於一些平臺上面沒有對線程局部存儲訪問的有效支持。由於JNIEnv指針是線程局部的,本地代碼決不能跨線程使用JNIEnv。

比起寫死一個函數入口來講,使用接口指針能夠有如下幾個優勢:

  1. JNI函數表是做爲參數傳遞給每個本地方法的,這樣的話,本地庫就沒必要與特定的JVM關聯起來。這使得JNI能夠在不一樣的JVM間通用。
  2. JVM能夠提供幾個不一樣的函數表,用於不一樣的場合。好比,JVM能夠提供兩個版本的JNI函數表,一個作較多的錯誤檢查,用於調試時;另一個作較少的錯誤檢查,更高效,用於發佈時。

像int、char等這樣的基本數據類型,在本地代碼和JVM之間進行復制傳遞,而對象是引用傳遞的。每個引用都包含一個指向JVM中相應的對象的指針,但本地代碼不能直接使用這個指針,必須經過引用來間接使用。

比起傳遞直接指針來講,傳遞引用可讓VM更靈活地管理對象。好比,你在本地代碼中抓着一個引用的時候,VM那小子可能這個時候正偷偷摸摸地把這個引用間接指向的那個對象從一起內存區域給挪到另外一塊兒。不過,有一點兒你放心,VM是不敢動對象裏面的內容的,由於引用的有效性它要負責。

本地代碼中,能夠經過JNI建立兩種引用,全局引用和局部引用。局部引用的有效期是本地方法的調用期間,調用完成後,局部引用會被JVM自動剷除。而全局引用呢,只要你不手動把它幹掉,它會一直站在那裏。

JVM中的對象做爲參數傳遞給本地方法時,用的是局部引用。大部分的JNI函數返回局部引用。JNI容許程序員從局部引用建立一個全局引用。接受對象做爲參數的JNI函數既支持全局引用也支持局部引用。本地方法執行完畢後,向JVM返回結果時,它可能向JVM返回局部引用,也可能返回全局引用。

局部引用只在建立它的線程內部有效。本地代碼不能跨線程傳遞和使用局部引用。

JNI中的NULL引用指向JVM中的null對象。對一個全局引用或者局部引用來講,只要它的值不是NULL,它就不會指向一個null對象。

一個對象從JVM傳遞給本地方法時,就把控制權移交了過去,JVM會爲每個對象的傳遞建立一條記錄,一條記錄就是一個本地代碼中的引用和JVM中的對象的一個映射。記錄中的對象不會被GC回收。全部傳遞到本地代碼中的對象和從JNI函數返回的對象都被自動地添加到映射表中。當本地方法返回時,VM會刪除這些映射,容許GC回收記錄中的數據。下圖演示了局部引用記錄是怎麼樣被建立和刪除的。一個JVM窗口對應一個本地方法,窗口裏麪包含了一個指向局部引用映射表的指針。方法D.f調用本地方法C.g。C.g經過C函數Java_C_g來實現。在進入到Java_C_g以前,虛擬機會建立一個局部引用映射表,當Java_C_g返回時,VM會刪掉這個局部引用映射表。

有許多方式能夠實現一個映射表,好比棧、表、鏈表、哈希表。實現時可能會使用引用計數來避免重得。

JNI容許本地代碼經過名字和類型描述符來訪問JAVA中的字段或調用JAVA中的方法。例如,爲了讀取類cls中的一個int實例字段,本地方法首先要獲取字段ID:

jfieldID fid = env->GetFieldID(env, cls, "i", "I");

而後能夠屢次使用這個ID,不須要再次查找:

jint value = env->GetIntField(env, obj, fid);

除非JVM把定義這個字段和方法的類或者接口unload,字段ID和方法ID會一直有效。字段和方法能夠來自定個類或接口,也能夠來自它們的父類或間接父類。JVM規範規定:若是兩個類或者接口定義了相同的字段和方法,那麼它們返回的字段ID和方法ID也必定會相同。例如,若是類B定義了字段fld,類C從B繼承了字段fld,那麼程序從這兩個類上獲取到的名字爲"fld"的字段的字段ID是相同的。JNI不會規定字段ID和方法ID在JVM內部如何實現。經過JNI,程序只能訪問那些已經知道名字和類型的字段和方法。而使用Java Core Reflection機制提供的API,程序員不用知道具體的信息就能夠訪問字段或者調用方法。有時在本地代碼中調用反射機制也頗有用。因此,JDK提供了一組API來在JNI字段ID和java.lang.reflect.Field類的實例之間轉換,另一組在JNI方法ID和java.lang.reflect.Method類實例之間轉換。

附:類型映射

相關文章
相關標籤/搜索