層級: 初級 Martyn Honeyford, 軟體工程師, IBM 英國實驗室
2002 年 1 月 18 日
一開始引入 Java 原生編譯時,它似乎一定能勝過 JVM,拋棄 Java 平台極力爭取的平台無關性。但是即使原生編譯越來越流行,並且市場上的原生編譯器越來越多,它要真正取代 Java 的可移植性還有一段路要走。不幸的是,就連該技術成熟到足以解決目前讓許多人頭疼的 Java 效能問題也還尚需時日。請在 論壇中將您對本文的想法與作者和其它讀者一起共用。
儘管 Java 語言有許多優點,但是仍然存在幾個問題限制了在關鍵項目中的使用。它們包括執行速度、記憶體佔用、磁碟佔用以及 JVM 可用性。雖然 JIT 編譯器極大改進了平台的執行速度,J2ME 大幅降低記憶體佔用,但是在許多領域中,Java 應用程式完全無法和它們原生競爭者(通常是 C/C++)競爭。為瞭解決這些問題,許多開發人員已經轉向使用 Java 原生編譯器,它們允許用 Java 語言編寫應用程式,然後將它們編譯成本機可執行程式。這種解決方案將以平台無關性為代價,但是它可以導致更快的執行速度和更小的記憶體佔用,這些對於當今許多應用程式都很關鍵。 為使您能快速掌握 Java 原生編譯技術,我們將首先討論代碼編譯基礎,包括為什麼許多開發人員正在使用 Java 原生編譯器編譯他們的應用程式的簡要概述。接下來,我們將使用自由軟體編譯器和兩個不同的應用程式(一個很簡單,另一個複雜一些)來測試 Java 原生編譯的結果。這些樣本和所產生的度量結果將作為研究如何比較最新的 Java 原生編譯器和 JVM 的第一手資料。 代碼編譯基礎 要理解本文討論的內容,您應該熟悉三種最常用的代碼編譯方法:
- 使用 Java 編譯器(例如,javac)編譯 Java 代碼
- 編譯機器碼,例如針對特定硬體/作業系統(OS)平台的 C/C++
- 使用針對特定硬體/OS 平台的 Java 原生編譯器來編譯 Java 代碼
使用 Java 編譯器編譯 Java 代碼是最簡單的。我們只要用 Java 語言編寫原始碼,使用 Java 編譯器將原始碼編譯成 Java 位元組碼,然後就可以在任何安裝了 JVM 的硬體/OS 平台上執行結果了。其缺點是 Java 依賴於 JVM 來實現其特點“一次編寫,隨處運行”可移植性;不僅要在運行 Java 應用程式的任何平台上安裝有可用的 JVM,而且必須還有大量系統資源(記憶體和磁碟空間)用以支援 JVM。因此,許多開發人員仍然依靠不太靈活但卻更具針對性的語言,例如,C/C++。 編譯 C/C++ 來源程式與編譯 Java 來源程式相似。只要編寫了代碼,我們通過一個針對特定硬體/OS 平台的編譯器和連結器來運行它。只有在目標平台上才可以執行產生的應用程式,但是不需要安裝 JVM(雖然它可能需要一些支援共用庫,這取決於所使用的語言)。幾乎使用這種方法開發的最簡單的應用程式都必須針對每個要運行它們的硬體/OS 平台單獨定製。 第三種方法嘗試結合以上兩種解決方案的優點,允許開發人員使用 Java 語言編寫應用程式,然後將它們編譯成本機可執行程式。編寫了 Java 代碼之後,就可以通過 Java 編譯器產生 Java 位元組碼,然後將 Java 位元組碼編譯成機器碼來運行它,或者在 Java 原生編譯器中直接運行 Java 代碼。需要的步驟數取決於所使用的編譯器的需求。 這種方法的優點是可以在 未安裝 JVM的目標平台上執行結果代碼。這樣做的目的是使 Java 應用程式以更快的速度執行,大幅降低運行所需的磁碟空間和記憶體(雖然有必要為 Java 原生編譯器提供支援資產庫)。 編譯器的目標平台、它們提供的 Java 支援層級以及它們使用的系統資源的數量都是不相同的。在本文的 參考資料一節中可以找到一些當前可用的原生編譯器的清單。
關於測試設定 對市場上每種原生編譯器的功能組件和效能進行比較已經大大超越了本文的範圍。我使用一種編譯器 ― GNU 編譯器 Java 程式設計語言版(GNU Compiler for the Java Programming Language, GCJ)作為樣本來詳細說明原生編譯的過程與結果。GCJ 是一種為 GNU 編譯器集(GNU Compiler Collection,GCC)開發的編譯器,GNU 編譯器集是 GNU 項目的一部分。與其它出自 GNU 項目的所有軟體一樣,GCJ 是雙重意義上的自由軟體,因此可以很容易地擷取(請參閱 參考資料)。如果您正在認真考慮您產品的原生編譯途徑,當然應該儘可能多地評估編譯器,或許可以使用本文中建立的標準。 我的測試系統硬體是一台裝有 450 MHz Pentium II 處理器和 320 MB 記憶體的 PC 機。作業系統是最近安裝的 Mandrake 8.1 Linux 分發版。這個分發版帶有 GCJ 的 3.0.1 版本,它包含在 GCC 3.0.1 中並且作為 8.1 Mandrake 分發版一部分提供。 我已經運行了兩個獨立的應用程式,一個很簡單,另一個複雜一些。為了比較系統和 Java 平台的效能,我將應用程式編譯成 Java 位元組碼。我使用 Sun JDK 版本 1.3.1.02 Linux 版來編譯 Java 代碼,然後在下列 JVM 上測試結果類:
- Kaffe 1.0.6
- Sun JVM 1.3.1_02
- IBM JRE 1.3.1
為實現本文的目的,我測量了執行速度、執行記憶體開銷和磁碟空間。
測試 1:Prime.java 第一個測試應用程式很簡單,由單一類 prime.java 組成。這個應用程式實現一個非常基本的搜尋質數的演算法。 清單 1 顯示了 prime.java 的原始碼。 清單 1. prime.java 的原始碼
import java.io.*;class prime{ private static boolean isPrime(long i) { for(long test = 2; test < i; test++) { if(i%test == 0) { return false; } } return true; } public static void main(String[] args) throws IOException { long start_time = System.currentTimeMillis(); long n_loops = 50000; long n_primes = 0; for(long i = 0; i < n_loops; i++) { if(isPrime(i)) { n_primes++; } } long end_time = System.currentTimeMillis(); System.out.println(n_primes + " primes found"); System.out.println("Time taken = " + (end_time - start_time)); }}
|
如您所見,代碼從 0 迴圈到 50000。運行時,它嘗試用每個它遇到的數除以每個小於這個數的數字,以找出是否有餘數。(不可否認,這是搜尋質數的一種蠻力方法,但是它可以滿足該樣本的需要。) 我使用下列命令將 prime.java 編譯成本機可執行程式:
gcj prime.java -O3 --main=prime -o prime
|
參數 -O3 表示“最佳化速度”;參數 --main 告訴 GCJ 哪一個類包含運行應用程式時將使用的 main 方法;參數 -o Prime 命名產生的可執行程式。有關一套完整的命令列參數,請參閱 GCJ 文檔。 為了編譯 Java 位元組碼測試,我使用了下列命令:
/usr/java/jdk1.3.1_02/bin/javac -O prime.java
|
接下來,對於每個測試 JVM,我使用下列命令調用代碼:
- 本機:
./prime
- Kaffe:
/usr/bin/java prime
- Sun JDK:
/usr/java/jdk1.3.1_02/bin/java prime
- IBM JRE:
/opt/IBMJava2-13/jre/bin/java prime
prime.java 的測試結果 如前所述,我測試了執行速度、記憶體使用量和磁碟空間使用方式。下表詳細說明了第一個測試的結果。 表 1. Prime.java:執行速度
實現 |
以毫秒為單位的時間(三次啟動並執行平均值 ― 分值越低越好) |
本機 |
40180 |
Kaffe |
75456 |
Sun JDK |
67315 |
IBM JRE |
18188 |
表 2. Prime.java:記憶體使用量
實現 |
VM 大小(KB) |
VM RSS(KB) |
本機 |
7024 |
3528 |
Kaffe |
8888 |
3564 |
Sun JDK |
169560 |
6636 |
IBM JRE |
81936 |
6288 |
請注意,VM 大小等於進程映象的總和。這包括該進程所使用的所有代碼、資料和共用庫,包括已經交換出去的頁面。VM 駐留集大小(RSS)等於實際駐留在 RAM 中的進程(代碼和資料)部分的大小,包括共用庫。這給出了一個進程大概使用多少 RAM 的近似值。 簡單來說,如果一個進程分配了大量記憶體,這會顯示在 VM 大小中,但是直到真正被使用(例如,讀或寫)時才會顯示在 VM RSS 中。實際上,VM RSS 是更重要的測量指標,因為它更準確地反映了系統的效能。 表 3. Prime.java:磁碟空間使用
實現 |
編譯的大小總合(位元組) |
本機 |
22268 |
Java 類 |
962 |
請注意,表 3 中顯示的測量不包含共用庫和 JVM,並且是剝去可執行程式後進行測量的。
測試 2:SciMark 2 對於第二個測試,我使用了一個更複雜的 Java 應用程式 SciMark 2 Java 基準測試程式。可以免費獲得本文使用的命令列版本(請參閱 參考資料)。SciMark 2 是一個很複雜的應用程式。它實現了許多用來準確地評測 JVM 的效率的基準測試程式。 我使用下列命令將 SciMark 2 編譯成本機可執行程式:
gcj-3.0.1 -O3 commandline.java Random.java FFT.java SOR.java Stopwatch.java SparseCompRow.java LU.java kernel.java MonteCarlo.java --main=jnt.scimark2.commandline -o scimark
|
然後,我使用下列命令將應用程式編譯成 Java 位元組碼:
/usr/java/jdk1.3.1_02/bin/javac -O *.java
|
SciMark 2 基準測試程式能以兩種模式運行,正常模式和大模式。您使用的模式決定使用的問題集的大小。我已經用這兩種模式運行了測試。 我使用下列命令以正常模式調用代碼:
- 本機:
./scimark
- Kaffe:
/usr/bin/java jnt.scimark2.commandline
- Sun JDK:
/usr/java/jdk1.3.1_02/bin/java jnt.scimark2.commandline
- IBM JRE:
/opt/IBMJava2-13/jre/bin/java jnt.scimark2.commandline
對於更大的問題集,我使用下列命令:
- 本機:
./scimark -large
- Kaffe:
/usr/bin/java jnt.scimark2.commandline -large
- Sun JDK:
/usr/java/jdk1.3.1_02/bin/java jnt.scimark2.commandline -large
- IBM JRE:
/opt/IBMJava2-13/jre/bin/java jnt.scimark2.commandline -large
SciMark 2 的測試結果 以下各表顯示了編譯 SciMark 2 的結果。請注意結果中正常模式與大模式的區別。 表 4. SciMark 2,正常模式:執行速度
實現 |
複合分值(三次啟動並執行平均值 ― 分值越高越好) |
本機 |
15.22 |
Kaffe |
7.01 |
Sun JDK |
22.86 |
IBM JRE |
25.29 |
表 5. SciMark 2,正常模式:記憶體使用量
實現 |
VM 大小(KB) |
VM RSS(KB) |
本機 |
9788 |
5956 |
Kaffe |
8888 |
4092 |
Sun JDK |
169692 |
7428 |
IBM JRE |
81964 |
7408 |
表 6. SciMark 2,大模式:執行速度
實現 |
複合分值(三次啟動並執行平均值 ― 分值越高越好) |
本機 |
8.78 |
Kaffe |
5.72 |
Sun JDK |
12.04 |
IBM JRE |
15.04 |
表 7. SciMark 2,大模式:記憶體使用量
實現 |
VM 大小(KB) |
VM RSS(KB) |
本機 |
62888 |
59072 |
Kaffe |
58056 |
56988 |
Sun JDK |
169692 |
64624 |
IBM JRE |
81964 |
57704 |
表 8. SciMark 2:兩種模式使用的磁碟空間
實現 |
編譯大小(位元組) |
本機 |
49588 |
Java 類 |
16318 |
再次聲明,表 8 中的測量不包括共用庫和 JVM,並且剝去了可執行程式後測量的。
原生編譯的優缺點 從上面的測試結果應該明顯看出,Java 原生編譯究竟是成功還是失敗還難有定論。某些基準測試程式顯示在原生編譯的可執行程式比使用某些 JVM 版本快;另外一些則相反。同樣,不同 JVM 之間,某些操作的速度也大相徑庭。執行的“工作集”記憶體測試顯示執行時在記憶體使用量方面並沒有很大差別。要進一步探索該領域,可以採用不同的垃圾資訊收集方案來進行本機和 JVM 測試。 本機版本只是在磁碟空間使用方面明顯優於 JVM 版本,而且只有當考慮到 JVM 的大小時,這才能成立。雖然類本身很小,但是所測試的 JVM 卻很大(IBM 和 Sun JVM 的 jre 子目錄中的遞迴目錄清單顯示僅是 JRE 就佔用了 50 MB 磁碟空間)。但是,請不要忘記還可以使用許多更小的 JVM,雖然 JVM 和單個應用程式的組合比本機可執行程式與 GCJ 執行階段程式庫 libgcj.so(少於 3 MB)的組合大得多,但是本機版本的可執行程式的大小卻大很多。因此,在需要大量應用程式的情況下,JVM 版本可能是最終的贏家。 除了這些有點模糊的結果之外,使用 Java 原生編譯還可能產生許多潛在問題。它們是:
- 失去了平台無關性:實際上,這並不是什麼大不了的問題。因為來源程式是用 Java 語言編寫的,所以您仍然可以選擇產生可以在任何地方啟動並執行 Java 位元組碼版本,然後按需要在特定平台上使用原生編譯器。
- 類支援/編譯器成熟度等級:某些編譯器仍然相對不太成熟,並且可能無法支援您應用程式所需的所有 Java 類。例如,雖然 GCJ 支援大部分上至 1.1 版本規範的 Java 語言構造,但是,它 不支援通常與 JVM 一起提供的所有 Java 類庫。最重要的是,它幾乎不支援 AWT,這使 GCJ 無法適用於 GUI 應用程式。不同的編譯器支援不同層級的類庫;Excelsior JET 聲稱是完全支援 AWT 和 Swing 的編譯器。
- 支援/複雜程度:因為這個領域相對較新,所以開發人員常常無法很好地瞭解它。診斷工具可能在基礎上有點薄弱,所以要診斷原生編譯的 Java 應用程式中發生的問題可能更困難(尤其當 Java 位元組碼版本中沒有發生該錯誤時)。
結束語 當開發應用程式時,通常確定 Java 原生編譯是否適合您的特定環境的唯一實際方法是完成一個問題解決周期。
- 確定希望使用原生編譯來解決的確切問題。
- 研究可用的原生編譯器,然後提出一些可能解決問題的原生編譯器。
- 嘗試您選擇的所有編譯器來編譯您的應用程式,然後觀察結果。
儘管 Java 原生編譯技術還比較稚嫩,並且缺少明確的結果,但它卻是 Java 語言中一個激動人心的新領域。利用現有選項的最佳方法是,或許可以使用本文中建立的某些方法和標準親自研究和測試 Java 原生編譯。 雖然原生編譯不會取代 JVM(許多人認為它會取代 JVM),但是已經證明,對於某些應用程式和環境,它是正確的解決方案。原生編譯將 Java 語言的使用範圍擴充到一些新領域,短短几年前在這些領域中還無法應用 Java 語言。整體而言,這僅對於 Java 語言和 Java 社區是件好事。
參考資料
- 您可以參閱本文在 developerWorks 全球網站上的 英文原文.
- 請參加關於本文的 論壇。
- 請訪問 GNU Compiler for the Java Programming Language首頁以瞭解有關 GCJ 和 GNU 編譯器集合的更多資訊。
- SciMark 2.0 是用來評測科學和工程應用程式中數學計算代碼效能的基準測試程式的組合。請訪問 SciMark 2.0 首頁以瞭解更多關於這種複雜應用程式的資訊。
- 從“ 彌補和 COM 的縫隙”(developerWorks,2001 年 10 月)中瞭解如何重用不是用 Java 語言編寫的代碼。
- 當無法在應用程式中採用一個純 Java 語言解決方案時,仍然可以有效地調試 Java/C 混合體。Matthew White 在“ 調試整合的 Java 和 C/C++ 代碼”(developerWorks,2001 年 11 月)中解釋了如何完成該操作。
- 在 developerWorks Java 技術專區中可以找到更多 Java 參考資料。
可選擇的 Java 原生編譯器
- GCJ
- BulletTrain
- Excelsior JET
- JOVE
- TowerJ
- Visual Cafe
- VisualAge for Java
- FastJ
關於作者
|
|
|
Martyn Honeyford 1996 年畢業於諾丁漢大學,獲電腦科學學士學位。從那時起,他就成為位於英格蘭 Hursley 的 IBM 英國實驗室的一名軟體工程師。他目前的職務是 WebSphere MQ Everyplace Team Dev中的一名開發人員。不工作的時候,他經常去彈電吉他(彈得很差)或者瘋狂地玩電子遊戲。可以通過 martynh@uk.ibm.com 與 Martyn 聯絡。 |
|