要真正彼此隔離 Java 應用程序,實質上需要多個 JVM,然而啟動成本和內存占用使這種方式不那麼理想。而共享類可以同時解決這兩個問題。在多 JVM 環境中,共享類通過將一組核心系統類裝載到共享內存中,可以在多個 JVM 中共享這些類。這些共享類放到內存的一個共享區域中,它們在這裡對所有 JVM 都是保持一致的。結果,共享類只需要在第一次使用時裝載到內存中,這消除了在以後每次 JVM 調用時裝載它們的固定成本,並減少了每個 JVM 中的內存占用。
IBM 在 z/OS 平台上實現了共享類技術。Apple Computer Inc. 在 Mac OS X 上實現了名為 Java Shared Archive(JSA)的一種共享類,而 Sun 在 J2SE 1.5 版中引入了基於 JSA 技術的 Class Data Sharing (CDS)。讓我們分析一下這些實現是如何工作的。
IBM 的實現
自 J2SE 1.3.1 以來,IBM 就在 z/OS 平台上提供了共享類技術的實現。這種實現是通過讓一個主(或稱 master) JVM 將核心系統類裝載到共享內存完成的,那麼這到底是什麼意思呢?
分解堆
內存分為共享堆和 Java 堆。主 JVM 將系統堆(即共享堆)分配到共享內存中,這裡是放置系統類的地方。系統堆在主 JVM 的生存周期中一直存在,並且不會受到垃圾收集(GC)的影響。每一個後續(或者 worker)JVM 附加到這個系統堆上,如圖 1 所示,並為自己的 Java 堆分配非共享內存,它會受垃圾收集的影響。Java 堆包含特定於每一個 worker JVM 運行的應用程序的非共享類和所有實例化的對象。
圖 1. 共享類分解堆
共享類裝載器
每個 worker JVM 都可以通過將類放到共享類裝載器的 classpath 中而將它們裝載到共享堆中。共享類與普通類的裝載方式一樣——使用 parent-delegation 模式。
層次結構中的每一個類裝載器檢查其緩存,確定這個類是否已經裝載。如果還沒有裝載,那麼類裝載器就向其父類裝載器傳遞一個檢查裝載請求,這樣一直上溯到層次結構頂部的 primordial 或者 bootstrap 類裝載器。如果沒有在任何緩沖區中發現這個類,那麼每一個類裝載器都會試圖從自己的存儲庫中裝載這個類,如果成功,就返回這個類。否則,它將請求傳遞給層次結構中下面的裝載器。這種模型保證了首先檢查最受信任的存儲庫,並防止信任程度低的代碼通過采用與核心 API 成員相同的名字代替受信任的核心 API 類。
如果類是 primordial 類或者定義的類裝載器是共享類裝載器,那麼類對象將在共享堆中創建,並且類標記為共享類。圖 2 顯示了 bootstrap 類裝載器位於類裝載器層次結構的頂部,並負責裝載核心 API 中的類。這些類是信任程度最高的。擴展類裝載器裝載 extensions 目錄中的標准擴展 JAR 文件中的類。共享應用程序類裝載器可以用於共享用戶或者應用程序類。
圖 2. 類裝載器層次結構
但是在這種實現中,類到底是如何由多個 JVM 共享的呢?
假定 JVM 1 裝載了 java/lang/String,這是一個由 bootstrap 類裝載器裝載的系統類。如果 JVM 2 想要裝載 java/lang/String,由於它不能訪問 JVM 1 的 bootstrap 類裝載器緩存,所以它必須使用自己的 bootstrap 類裝載器重新裝載這個類。在這個例子中,JVM 不共享任何類,如圖 3 所示。
圖 3. 類沒有跨 JVM 共享
因此,最好讓 JVM 共享相同的類,如圖 4 所描繪的。
圖 4. 跨 JVM 共享的類
要解決這個問題,通過創建一個名為 namespace 的全局類緩存,將類緩存的概念加以擴展。每個 JVM 的類裝載器必須在這個 namespace 上注冊。當共享類裝載器裝載一個類時,它被同時放到本地類緩存和 namespace 中(如圖 5 所示)。這樣做使得其他 JVM 中的類裝載器(在 namespace 上注冊的)不用裝載它就可以訪問這個類。
圖 5. Namespace 跨 JVM 共享
保護域
類裝載器有一個或者多個代碼源對象(從其中裝載類的 JAR 文件或者目錄)。這些對象用於創建保護域,它被傳遞給 defineClass() 方法調用。使用共享類的其他 JVM 將需要這個信息,但是不能共享保護域,因為它們包含本地信息。為了解決這個問題,將代碼源放到系統堆中。打包信息也需要共享。
競爭條件
由於 JVM 會讀取和寫入共享數據,需要有一種方法處理競爭條件。最簡單的方式是使用全局塊。不過,出於性能和伸縮性的原因,應當謹慎地使用它們。
一種避免鎖住所有 JVM 的方法是使用開放式原子更新(optimistic atomic updates)。例如,在裝載一個類時,類裝載器將檢查其 namespace (在檢查其自己的本地類緩存後)。如果不能找到這個類,那麼它就會裝載它。裝載後,它會自動檢查其他 JVM 沒有裝載這個類,然後更新 namespace。
全局與本地數據
類中有些信息(如名字)在所有 JVM 中都是一樣的,而另一些信息要求是本地的,如裝載這個類的類裝載器。 每個 JVM 都需要有裝載這個類時需要它生成的那一部分的本地副本,如圖 6 所示。JVM 中類的陰影區域是本地副本。系統中類的非陰影區域是全局部分。
圖 6.共享全局數據與共享本地數據
共享類時偶爾會出現的一個問題是,當一個 JVM 更新一個類時(比如通過修改靜態字段),所有其他 JVM 都會看到這種改變。這種操作是不希望的,它會造成不可預料的結果。為了保證隔離性,每個 JVM 都有每個共享類的所有靜態字段的副本。
JIT 編譯代碼
當屬於共享類的代碼由即時(just-in-time,JIT) 編譯器編譯時,它是自動共享的。這意味著不管由哪個 JVM 編譯代碼,所有 JVM 都會獲得性能上的好處(而只有一個承擔 JITing 的開銷)。
啟動器程序
IBM 的實現需要一個啟動器(launcher),以便控制 JVM 的創建。這個啟動器必須由用戶以本機代碼編寫。如清單 1 顯示了一個示例啟動器的偽代碼:
清單 1. 啟動器偽代碼
{
create Master JVM (and store returned token*);
while(work to do) {
create a Worker JVM passing in token from Master JVM;
do work on Worker JVM;
terminate Worker JVM;
}
terminate Master JVM;
}
* token returned from the Master JVM is the address of the shared heap.
Apple 的實現
Apple 的共享類技術實現是 Mac OS X 中的 Java 共享檔案(Java shared archive,JSA)。
實質上,JSA 是一個內存映射到共享內存的文件,可以讓多個進程(即多個 JVM )訪問它。安裝了 Java Runtime Environment (JRE) 後,用系統 JAR 文件中的類創建 JSA。這些核心類的內部表示存儲在文檔中。這個數據是靜態的,因此永遠也不會改變,這意味著可以共享這些類而不會有任何隔離問題。它還意味著這些類永遠不會被垃圾收集。因為 Apple JVM 使用一般性 GC,因此必須保護這些類不被收集。這種保護是通過引入 immortal 對象的概念來實現的,JSA 中的所有類都指定為 immortal。
這種技術在默認情況下是可用的,使用它不需要編寫任何特殊的程序——如 IBM 的實現所需要的啟動器。這種實現的另一個好處是系統重啟後也可以享受它的好處,不局限於特定 JVM 的生存周期。不過,它的缺點是局限於一組核心類,沒有提供共享應用程序類或者 JIT 代碼的能力。
Sun 的實現
Sun 的共享類技術的實現稱為類數據共享(Class Data Sharing,CDS),是 J2SE 1.5 中的新功能。CDS 基於 Apple 的 JSA 實現。
與 JSA 類似, CDS 使用一個只讀的內存映射文檔文件。這個文件包含核心系統類的內部表示,並在啟動時映射到每個 JVM 的 Java 堆中。CDS 文件既可以由 Sun JRE 安裝器創建,也可以像 Sun 的 CDS 文檔中所描述的那樣手工創建。
同樣,主要的好處是啟動時的成本節省和降低內存占用。啟動時間之所以減少是因為核心類不是用傳統的類裝載機制(即 JVM 一次映射所有核心類,而不是單獨裝載每一個類)裝載的。所使用的內存之所以減少是因為 JVM 共享了只讀類數據,而不用每一個 JVM 占有自己的副本。同時,因為類不是用傳統機制裝載的,所以 JVM 不會由處理任何未使用的方法。
在撰寫本文的時候,Sun 的實現只限於共享核心系統類,並且不允許共享應用程序級的類或者 JIT 代碼。應用程序相對於它所使用的核心類越小,啟動時節省越多。
根據 Sun 的 CDS 文檔,在將來共享類功能將擴展到應用程序級的類,以改進大型應用程序的啟動成本。
共享類的當前應用
Java 平台的用戶已經利用了共享類技術。本節描述當前部署的一些應用領域。
事務環境
共享類(不管是系統類還是應用程序類)對事務環境(如 CICS 和 DB2)中的 JVM 啟動和內存占用可以有顯著的效果,在這裡每個事務或者應用程序都包裝在單獨的 JVM 中。
z/OS 2.3 上的 Customer Information Control System Transaction Server (CICS TS) 是運行 Java 事務和應用程序的主要商業產品之一(目前使用了 IBM JVM 的共享類技術)。CICS TS 2.3 引入了共享類緩存工具,它將共享類功能擴展為它所控制的 JVM 池(稱為它的 JVMset)。
CICS 使用自定義啟動器程序來控制 master JVM 以及所有 worker JVM 的啟動,它們需要服務請求來運行 Java 組件。除了系統堆中的共享類,它們還可以共享 worker JVM 中的特殊“中間件類”和一些“應用程序類”,如圖 7 所示。這個共享類緩存工具為 CICS 客戶提供了很多好處。例如,Java 類是由 master JVM 裝載的,每個 CICS 區域(當 CICS 啟動時) 裝載一次而不是每個 JVM 一次,從而在 JVMset 中減少所有 worker 的類裝載成本。通過在緩沖區中保留每個類的一個副本而不是每個 worker JVM 堆一個副本,共享類緩存工具還減少了 JVMset 的總體存儲需求。
圖 7. CICS 中的共享類
在處理大量事務時,由更快的啟動和更有效的內存處理而獲得的好處會顯著增加。圖 8 中第一個圖顯示了共享和非共享 CICS 環境中一個簡單事務的啟動時間。第二個圖顯示整體 JVM 存儲成本,也分為共享和非共享 CICS 環境。可以明顯地看出當 JVM 的數量增加時,每個 JVM 所需要的內存減少了(與非共享比較)。
圖 8. 使用共享類的 CICS 性能圖
桌面系統
在 Apple Mac OS X 平台上運行的 Java 應用程序可以自動從這種技術中獲得好處,Sun J2SE 1.5 用戶也會從中受益。
共享類將來可能的應用
共享類技術有可能為基於 Java 平台的其他技術的用戶帶來極大的好處。本節重點介紹一些可能的應用。
普及環境
談到運行 Java 應用程序,更小的內存占用對普及計算環境(如 PDA 和移動電話)會有顯著的影響。這種環境是由特殊的 VM、JIT 編譯器和 J2ME 中的類構成的。不過,當普及設備變得越來越普遍時,需要多 JVM 的可能性會增加。使用包含核心系統類的內存映射文件會顯著節省內存,這對於普及設備是很關鍵的。
網格計算
在網格計算環境中,像 CPU 時間和內存容量這樣的資源決定了所發生的成本,優化通常是有好處的。
使用共享類可以為網格提供者提供更好的運行 Java 程序的能力,因此它們可以同時為用戶運行更多的任務。它還使客戶每次使用的成本更低,這使它們具有更高的價值,為供應商提供更大的競爭優勢。
Java 應用程序
對於復雜的 J2EE 應用程序,如 IBM WebSphere,可能會裝載數千個類。如果核心系統類和 WebSphere 類是共享的(而不是由每個 JVM 在使用前裝載),那麼應用程序啟動時間可以有顯著縮短。這種好處加上內存占用減少可以使 WebSphere 上運行的各個應用程序受益。
對於 Java 用戶,讓應用程序更快地啟動的同時保持更低的內存占用肯定是有好處的。如果在啟動 Java 應用程序(如 Eclipse)時核心類已經裝載,那麼這些應用程序可以共享核心類,因而不用在啟動時單獨裝載它們。這種方式對於使用 Swing 或者 AWT 應用程序特別有用,這些應用程序已經由於啟動緩慢和占用大量內存而受到報抱怨了。
結束語
本文提供了對 Java 共享類技術的概述和一般性介紹。我們展示了不同的共享類和它們所提供的好處,如 Java 應用程序的啟動時間更快和內存占用更小。我們還分析了可以利用共享類的當前和未來的一些技術。
所有需要關注啟動時間和內存占用的 Java 應用程序都可以通過共享類技術獲益。當前的實現有局限性,如有限的能力或者不能共享應用程序類(或者這兩者)。如果能夠解決這些問題,那麼更多用戶會從這種技術中受益,使 Java 應用程序更有吸引力。