阻礙Java獲得廣泛應用的一個主要因素是Java程式的運行效率。Java是介於解釋型和編譯型之間的一種語言,同樣的程式,如果用編譯型語言C來實現,其運行速度一般要比Java快一倍以上。Java具有平台無關性,這使人們在開發企業級應用的時候總是把它作為主要候選方案之一,但是效能方面的因素又大大削弱了它的競爭力。為此,提高Java的效能就顯得十分重要。
問題的提出
Sun公司及Java的支援者們為提高Java的運行速度已經做出了許多努力,其中大多數集中在程式設計的方法和模式選擇方面。由於演算法和設計模式的最佳化是通用的,對Java有效最佳化演算法和設計模式,對其他編譯語言也基本同樣適用,因此不能從根本上改變Java程式與編譯型語言在執行效率方面的差異。
JIT(Just In Time,及時編譯)技術是個比較好的思想。它的基本原理是:首先通過Java編譯器把Java原始碼編譯成平台無關的二進位位元組碼。然後在Java程式真正執行之前,系統通過JIT編譯器把Java的位元組碼編譯為本地化機器碼。最後,系統執行本地化機器碼,節省了對位元組碼進行解釋的時間。這樣做的優點是大大提高了Java程式的效能,縮短了載入程式的時間;同時,由於編譯的結果並不在程式運行間儲存,因此也節約了儲存空間。缺點是由於JIT編譯器對所有的代碼都想最佳化,因此同樣也佔用了很多時間。
動態最佳化技術是提高Java效能的另一個嘗試。該技術試圖通過把Java來源程式直接編譯成機器碼,以充分利用Java動態編譯和靜態編譯技術來提高Java的效能。該方法把輸入的Java源碼或位元組碼轉換為經過高度最佳化的可執行代碼和動態庫 (Windows中的. dll檔案或Unix中的. so檔案)。該技術能大大提高程式的效能,但卻破壞了Java的可移植性。
JNI技術
實際上,有一種通常為我們忽視的技術可以在很大程度上解決這個難題,那就是JNI(Java Native Interface, Java本地化方法)。主張採用純Java的人們通常反對本地化代碼的使用,他們認為在Java程式執行的過程中調用C/C++程式會影響程式的可移植性和安全性。還有一些人認為JNI只是對過去混合編程技術的簡單擴充,其實際目的是為了充分利用大量原有的C程式庫。
其實,我們不必拘泥於嚴格的平台獨立性限制,因為採用JNI技術只是針對一些嚴重影響Java效能的程式碼片段,該部分可能只佔來源程式的極少部分,所以幾乎可以不考慮該部分代碼在主流平台之間移植的工作量。同時,也不必過分擔心類型匹配問題,我們完全可以控制碼不出現這種錯誤。此外,也不必擔心安全控制問題,因為Java安全模型已擴充為允許非系統類別載入和調用本地方法。根據Java規範,從JDK 1. 2開始,FindClass將設法找到與當前的本地方法關聯的類載入器。如果平台相關代碼屬於一個系統類別,則無需涉及任何類載入器; 否則,將調用適當的類載入器來載入和連結已命名的類。換句話說,如果在Java程式中直接調用C/C++語言產生的機器碼,該部分代碼的安全性就由Java虛擬機器控制。
JNI實現步驟
編寫JNI代碼的大致流程如所示:
JNI實現流程圖
1. 首先編寫需要JNI功能的Java類源檔案。其中,需要JNI實現的方法應當用native關鍵字聲明。在該類中,用System. loadLibrary()方法載入需要的動態連結程式庫。關鍵代碼如下:
//Compute.java
……
public class Compute {
public native double comp (double [] params);
……
static {
// 調用動態連結程式庫
System. loadLibrary(“mathlib”);
}
……
}
2. 將該類源檔案用Java類編譯器編譯成二進位位元組碼檔案。由於採用了native關鍵字聲明,編譯器會忽視沒有代碼體的JNI方法部分。
3. 利用javah -jni *.class 產生相關JNI方法的標頭檔。我們可以手工產生該檔案,但是由於Java虛擬機器是根據一定的命名規範完成對JNI方法的調用,所以手工編寫標頭檔需要特別小心。
上述檔案產生的標頭檔部分代碼如下:
//Compute. h
……
extern “C” {
JNIEXPORT jdouble JNICALL Java_Compute_comp (JNIEnv *, jobject, jdoubleArray);
}
……
可以看出,JNI函數名稱分為三部分:首先是Java關鍵字,供Java虛擬機器識別;然後是調用者類名稱(全限定的類名,其中用底線代替名稱分隔字元);最後是對應的方法名稱,各段名稱之間用底線分割。
JNI函數的參數也由三部分組成: 首先是JNIEnv *,是一個指向JNI運行環境的指標;第二個參數隨本地方法是靜態還是非靜態而有所不同——非靜態本地方法的第二個參數是對對象的引用,而靜態本地方法的第二個參數是對其 Java 類的引用; 其餘的參數對應通常 Java 方法的參數,參數類型需要根據一定規則進行映射。
4. 根據標頭檔編寫相應方法的實現代碼。由於篇幅所限,具體的實現部分在此不再贅述。在編碼過程中,需要注意變數的長度問題,例如Java的整型變數長度為32位,而C語言為16位,所以要仔細核對變數類型映射表,防止在傳值過程中出現問題。
5. 利用C/C++編譯器將JNI實現代碼編譯成動態連結程式庫。調用者類中需要顯式調用該連結庫。
在Win32環境下,可以利用Visual C ++或其他能產生DLL檔案的C/C++編譯器將實現代碼編譯成動態連結程式庫。筆者利用的是Microsoft.NET Framework的編譯器。編譯指令如下,其中%Java_HOME%是筆者的jdk安裝目錄變數:
cl -I%Java_HOME%/include
-I%Java_HOME%/include/win32
-LD jnicomp. c -Femathlib. dll
在Sun Soloaris下,相應指令為:
cc -G -I/usr/local/java/include -I/usr/local/java/include/solaris jnicomp. c /
-o mathlib. so
注意,編譯的時候需要用I指令包含必要的庫檔案路徑。
經過上述處理,就基本上完成了一個包含本地化方法的Java類的開發。
JNI技術的應用
一些主要的Java技術,如JDBC和RMI,大部分都採用JNI方式實現。但是,採用JNI確實會影響程式的平台無關性,所以只能在特別需要的地方才能使用。通常來說,如果遇到下面的情況,我們可以考慮JNI:
● 需要直接操作物理裝置,而沒有相關的驅動程式,這時候我們可能需要用C甚至組合語言來編寫該裝置的驅動,然後通過JNI調用;
● 涉及大量數**算的部分,用Java會帶來些效率上的損失;
● 用Java會產生系統難以支付的開銷,如需要大量網路連結的場合;
● 存在大量可重用的C/C++代碼,通過JNI可以減少開發工作量,避免重複開發。
另外,在利用JNI技術的時候要注意以下幾點:
● 由於Java安全機制的限制,不要試圖通過Jar檔案的方式發布包含本地化方法的Applet到用戶端;
● 注意記憶體管理問題,雖然在本地方法返回 Java 後將自動釋放局部引用,但過多的局部引用將使虛擬機器在執行本地方法時耗盡記憶體;
● JNI技術不僅可以讓Java程式調用C/C++代碼,也可以讓C/C++代碼調用Java代碼。