Java 編程的動態性,第 1 部分:類和類裝入

(有刪減)

研究類以及 JVM 裝入類時所發生的情況
級別:中級

Dennis M. Sosnoski[email protected]
總裁,Sosnoski Software Solutions, Inc.
2003 年 6 月

裝入類
諸如 C 和 C++ 這些編譯成本機代碼的語言通常在編譯完源代碼之後需要鏈接這個步驟。這一鏈接過程將來自獨立編譯好的各個源文件的代碼和共享庫代碼合併起來,從而形成了一個可執行程序。Java 語言就不同。使用 Java 語言,由編譯器生成的類在被裝入到 JVM 之前通常保持原狀。即使從類文件構建 JAR 文件也不會改變這一點 — JAR 只是類文件的容器。

鏈接類不是一個獨立步驟,它是在 JVM 將這些類裝入到內存時所執行作業的一部分。在最初裝入類時這一步會增加一些開銷,但也爲 Java 應用程序提供了高度靈活性。例如,在編寫應用程序以使用接口時,可以到運行時才指定其實際實現。這個用於組裝應用程序的後聯編方法廣泛用於 Java 平臺,servlet 就是一個常見示例。

JVM 規範中詳細描述了裝入類的規則。其基本原則是隻在需要時才裝入類(或者至少看上去是這樣裝入 — JVM 在實際裝入時有一些靈活性,但必須保持固定的類初始化順序)。每個裝入的類都可能擁有其它所依賴的類,所以裝入過程是遞歸的。清單 2 中的類顯示了這一遞歸裝入的工作方式。Demo 類包含一個簡單的 main 方法,它創建了 Greeter 的實例,並調用 greet 方法。Greeter 構造函數創建了 Message 的實例,隨後會在 greet 方法調用中使用它。

清單 2. 類裝入演示的源代碼
public class Demo
{
    public static void main(String[] args) {
        System.out.println("**beginning execution**");
        Greeter greeter = new Greeter();
        System.out.println("**created Greeter**");
        greeter.greet();
    }
}

public class Greeter
{
    private static Message s_message = new Message("Hello, World!");
   
    public void greet() {
        s_message.print(System.out);
    }
}

public class Message
{
    private String m_text;
   
    public Message(String text) {
        m_text = text;
    }
   
    public void print(java.io.PrintStream ps) {
        ps.println(m_text);
    }
}

java 命令行上設置參數 -verbose:class 會打印類裝入過程的跟蹤記錄。清單 3 顯示了使用這一參數運行清單 2 程序的部分輸出:

清單 3. -verbose:class 的部分輸出

[Opened /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/sunrsasign.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jsse.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jce.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/charsets.jar]
[Loaded java.lang.Object from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.io.Serializable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.String from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
...
[Loaded java.security.Principal from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.security.cert.Certificate
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded Demo]
**beginning execution**
[Loaded Greeter]
[Loaded Message]
**created Greeter**
Hello, World!
[Loaded java.util.HashMap$KeySet
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.util.HashMap$KeyIterator
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]

這隻列出了輸出中最重要的部分 — 完整的跟蹤記錄由 294 行組成,我刪除了其中大部分,形成了這個清單。最初的一組類裝入(本例中是 279 個)都是在嘗試裝入 Demo 類時觸發的。這些類是每個 Java 程序(不管有多小)都要使用的核心類。即使刪除 Demo main 方法的所有代碼也不會影響這個初始的裝入順序。但是不同版本的類庫所涉及的類數量和名稱都不同。

在上面這個清單中,裝入 Demo 類之後的部分更有趣。這裏的順序顯示了只有在準備創建 Greeter 類的實例時纔會裝入該類。不過,Greeter 類使用了 Message 類的靜態實例,所以在可以創建 Greeter 類的實例之前,還必須先裝入 Message 類。

在裝入並初始化類時,JVM 內部會完成許多操作,包括解碼二進制類格式、檢查與其它類的兼容性、驗證字節碼操作的順序以及最終構造 java.lang.Class 實例來表示新類。這個 Class 對象成了 JVM 創建新類的所有實例的基礎。它還是已裝入類本身的標識 — 對於裝入到 JVM 的同一個二進制類,可以有多個副本,每個副本都有其自己的 Class 實例。即使這些副本都共享同一個類名,但對 JVM 而言它們都是獨立的類。

非常規(類)路徑
裝入到 JVM 的類是由類裝入器控制的。JVM 中構建了一個引導程序類裝入器,它負責裝入基本的 Java 類庫類。這個特殊的類裝入器有一些專門的特性。首先,它只裝入在引導類路徑上找到的類。因爲這些是可信的系統類,所以引導程序裝入器跳過了對常規(不可信)類所做的大量驗證。

引導程序不是唯一的類裝入器。對於初學者而言,JVM 爲裝入標準 Java 擴展 API 中的類定義了一個擴展類裝入器,併爲裝入一般類路徑上的類(包括應用程序類)定義了一個系統類裝入器。應用程序還可以定義它們自己的用於特殊用途(例如運行時類的重新裝入)的類裝入器。這樣添加的類裝入器派生自 java.lang.ClassLoader 類(可能是間接派生的),該類對從字節數組構建內部類表示(java.lang.Class 實例)提供了核心支持。每個構造好的類在某種意義上是由裝入它的類裝入器所“擁有”。類裝入器通常保留它們所裝入類的映射,從而當再次請求某個類時,能通過名稱找到該類。

每個類裝入器還保留對父類裝入器的引用,這樣就定義了類裝入器樹,樹根爲引導程序裝入器。在需要某個特定類的實例(由名稱來標識)時,無論哪個類裝入器最初處理該請求,在嘗試直接裝入該類之前,一般都會先檢查其父類裝入器。如果存在多層類裝入器,那麼會遞歸執行這一步,所以這意味着通常不僅在裝入該類的類裝入器中該類是可見的,而且對於所有後代類裝入器也都是可見的。這還意味着如果一條鏈上有多個類裝入器可以裝入某個類,那麼該樹最上端的那個類裝入器會是實際裝入該類的類裝入器。

在許多環境中,Java 程序會使用多個應用程序類裝入器。J2EE 框架就是一個示例。該框架裝入的每個 J2EE 應用程序都需要擁有一個獨立的類裝入器以防止一個應用程序中的類干擾其它應用程序。該框架代碼本身也將使用一個或多個其它類裝入器,同樣用來防止對應用程序產生的或來自應用程序的干擾。整個類裝入器集合形成了樹狀結構的層次結構,在其每個層次上都可裝入不同類型的類。

裝入器樹
作爲類裝入器層次結構的實際示例,圖 1 顯示了 Tomcat servlet 引擎定義的類裝入器層次結構。這裏 Common 類裝入器從 Tomcat 安裝的某個特定目錄的 JAR 文件進行裝入,旨在用於在服務器和所有 Web 應用程序之間共享代碼。Catalina 裝入器用於裝入 Tomcat 自己的類,而 Shared 裝入器用於裝入 Web 應用程序之間共享的類。最後,每個 Web 應用程序有自己的裝入器用於其私有類。

圖 1. Tomcat 類裝入器
Tomcat 類裝入器

在這種環境中,跟蹤合適的裝入器以用於請求新類會很混亂。爲此,在 Java 2 平臺中將 setContextClassLoader 方法和 getContextClassLoader 方法添加到了 java.lang.Thread 類中。這些方法允許該框架設置類裝入器,使得在運行每個應用程序中的代碼時可以將類裝入器用於該應用程序。

能裝入獨立的類集合這一靈活性是 Java 平臺的一個重要特性。儘管這個特性很有用,但是它在某些情況中會產生混淆。一個令人混淆的方面是處理 JVM 類路徑這樣的老問題。例如,在圖 1 顯示的 Tomcat 類裝入器層次結構中,由 Common 類裝入器裝入的類決不能(根據名稱)直接訪問由 Web 應用程序裝入的類。使這些類聯繫在一起的唯一方法是通過使用這兩個類集都可見的接口。在這個例子中,就是包含由 Java servlet 實現的 javax.servlet.Servlet

無論何種原因在類裝入器之間移動代碼時都會出現問題。例如,當 J2SE 1.4 將用於 XML 處理的 JAXP API 移到標準分發版中時,在許多環境中都產生了問題,因爲這些環境中的應用程序以前是依賴於裝入它們自己選擇的 XML API 實現的。使用 J2SE 1.3,只要在用戶類路徑中包含合適的 JAR 文件就可以解決該問題。在 J2SE 1.4 中,這些 API 的標準版現在位於擴展的類路徑中,所以它們通常將覆蓋用戶類路徑中出現的任何實現。

使用多個類裝入器還可能引起其它類型的混淆。圖 2 顯示了類身份危機(class identity crisis)的示例,它是在兩個獨立類裝入器都裝入一個接口及其相關的實現時產生的危機。即使接口和類的名稱和二進制實現都相同,但是來自一個裝入器的類的實例不能被認爲是實現了來自另一個裝入器的接口。圖 2 中通過將接口類 I 移至 System 類裝入器的空間就可以解除這種混淆。類 A 仍然有兩個獨立的實例,但它們都實現了同一個接口 I

圖 2. 類身份危機
類身份危機

結束語
Java 類定義和 JVM 規範一起爲運行時組裝代碼定義了功能極其強大的框架。通過使用類裝入器,Java 應用程序能使用多個版本的類,否則這些類就會引起衝突。類裝入器的靈活性甚至允許動態地重新裝入已修改的代碼,同時應用程序繼續執行。

這裏,Java 平臺靈活性在某種程度上是以啓動應用程序時較高的開銷作爲代價的。在 JVM 可以開始執行甚至最簡單的應用程序代碼之前,它都必須裝入數百個獨立的類。相對於頻繁使用的小程序,這個啓動成本通常使 Java 平臺更適合於長時間運行的服務器類型的應用程序。服務器應用程序還最大程度地受益於代碼在運行時進行組裝這種靈活性,所以對於這種開發,Java 平臺正日益受寵也就不足爲奇了。

在本系列文章的第 2 部分中,我將介紹使用 Java 平臺動態基礎的另一個方面:反射 API(Reflection API)。反射使執行代碼能夠訪問內部類信息。這可能是構建靈活代碼的極佳工具,可以不使用類之間任何源代碼鏈接就能夠在運行時將代碼掛接在一起。但象使用大多數工具一樣,您必須知道何時及如何使用它以獲得最大利益。請閱讀 Java 編程的動態性第 2 部分以瞭解有效反射的訣竅和利弊。

參考資料

關於作者
Dennis Sosnoski 的照片 Dennis Sosnoski 是西雅圖地區 Java 諮詢公司 Sosnoski Software Solutions, Inc. 的創始人和首席顧問,他是 J2EE、XML 和 Web 服務支持方面的專家。他已經有 30 多年專業軟件開發經驗,最近幾年他集中研究服務器端的 Java 技術。Dennis 經常在全國性的會議上就 XML 和 Java 技術發表演講,他還是 Seattle Java-XML SIG 的主席。可以通過 [email protected] 與 Dennis 聯繫。
發佈了18 篇原創文章 · 獲贊 0 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章