一個.class文件定義了JVM中的一個類型,包括域、方法、繼承信息、註解和其他元數據。規範中對類文件的格式有詳細描述,任何想在JVM上運行的語言都必須遵守。
類是平台能加載的最小程序代碼單元。要將新的類加入到JVM的當前運行態中,有幾步操作必須執行。首先,類文件必須被加載進來並連接,而且必須進行大量的驗證。之後會提供一個代表該類型的新Class
對像給正在運行的系統,並可以創建新的實例。
本節會討論所有這些步驟,並介紹一下類加載器,也就是控制整個過程的那些類。我們先來看看加載和連接。
5.1.1 加載和連接:概覽
JVM的目的是使用類文件並執行其中的字節碼。要實現這個目的,JVM必須以字節數據流的方式取出類文件中的內容,並將其轉換成可用的格式加入運行態中。這個分兩步走的過程被稱為加載和連接,但連接又會被分解為幾個子階段。
加載
這個過程首先要讀取構成類文件的字節數據流並給類的表現形式解凍。該過程一開始是創建一個字節數組,其內容通常是從文件系統中讀取的,然後產生與所加載的類對應的Class
對象。在這個過程中會對類做一些基本檢查,但在加載過程結束時,Class
對像還不成熟,所以類也不可用。
連接
加載完成之後,類必須被連接起來。這一步驟分為三個子階段——驗證,準備和解析。驗證階段證實類文件符合預期,不會引起系統的運行時錯誤或其他問題。之後是類的準備階段,在類文件中引用的其他類型全部都要定位到,以確保該類已準備就緒。
連接步驟中各子階段之間的相互關係如圖5-1所示。
圖5-1 加載與連接(及連接的子階段)
5.1.2 驗證
驗證是一個非常複雜的過程,它分為幾個步驟。
首先是完整性檢查。這實際上是加載過程中的一部分,會確保類文件格式良好,可以連接。
接著是檢查常量池(詳情參見5.3.3節)中的符號信息是自相一致的,並要遵守常量的基本行為準則。其他不涉及代碼的靜態檢查也要在這一階段完成,比如檢查final
方法有沒有被重寫。
之後是驗證中最複雜的部分——方法的字節碼檢查。要檢查字節碼行為良好,並且不會試圖擺脫VM的環境控制。下面是一些主要檢查。
- 是否所有方法都遵守訪問控制關鍵字的限定。
- 方法調用的參數個數和靜態類型是否正確。
- 確保字節碼不會試圖濫用堆棧。
- 確保變量使用之前被正確初始化了。
- 檢查變量是否僅被賦予恰當類型的值。
做這些檢查是出於性能方面的考慮,這樣可以加快解釋碼的運行速度,運行時就不用再做這些檢查了。同時還簡化了運行時把字節碼編譯為機器碼的過程(即時編譯,詳情參見6.6節)。
準備
類的準備包括分配內存和準備好初始化類中的靜態變量,但不會現在初始化變量,也不會執行任何VM字節碼。
解析
解析會促使VM檢查類文件中所引用的類型是不是都是已知的類型。如果有運行時未知的類型,那它們也需要被加載進來。這些可見的未知類型會再次引發類加載過程。
一旦需要加載的其他類型全被定位並解析完成,VM就可以初始化那個最初要加載的類。這時所有靜態變量都可以被初始化,所有靜態初始化代碼塊都會運行。現在你運行的字節碼就是來自新加載進來的類裡的。這一步完成之後,類的加載就已全部完成,類也就可以使用了。
5.1.3 Class對像
連接和加載過程的最終結果是一個Class
對象,用於表示加載並連接起來的新類型。儘管出於性能方面的考慮,Class
對像只是在要求的地方做了初始化,但現在它在VM中完全生效了。代碼可以繼續執行了,它可以使用新類型並創建新實例。此外,Class
對像提供了一些不錯的方法,比如getSuperClass
,可以用它返回Class
對象的父類。
Class
對象可以和反射API一起實現對方法、域、構造方法等類成員的間接訪問。Class
對像中有對類成員Method
和Field
對象的引用。反射API可以用這些對像實現對它們的間接訪問。圖5-2是這種結構的高層視圖。
圖5-2 Class對象與Method引用
運行時的哪個部分會定位並連接字節流以生成新的加載類?在下一個主題中,我們會討論這個問題,即能夠完成這些工作的類加載器,它是由抽像類ClassLoader
的子類們組成的。
5.1.4 類加載器
Java平台裡有幾個經典的類加載器,它們在平台的啟動和常規操作過程中承擔不同的任務:
根(或引導)類加載器——通常在VM啟動後不久實例化,一般用本地代碼實現。最好把它看做VM的一部分。它的作用通常是負責加載系統的基礎JAR(主要是
rt.jar
),而且它不做驗證工作。擴展類加載器——用來加載安裝時自帶的標準擴展。一般包括安全性擴展。
應用(或系統)類加載器——這是應用最廣泛的類加載器。它負責加載應用類。在大多數SE(Java標準版)的環境中,主要工作都是由它來完成。
定制類加載器——在更複雜的環境中,比如EE(Java企業版)或比較複雜的SE框架,通常會有些附加(即定制)的類加載器。有些團隊甚至為他們的某個應用程序編寫了特定的類加載器。
除了核心任務,類加載器還經常要從JAR文件或classpath中加載資源(不是類文件,比如圖片或配置文件)。
例子——工具類加載器
在EMMA測試覆蓋工具(http://emma.sourceforge.net/)中使用的一個類加載器可以作為加載時轉化的例子。
當為了加上額外的測試輔助信息而加載類時,EMMA的類加載器會修改字節碼。當在這些代碼上運行測試用例時,EMMA會記錄測試用例實際測試了哪些方法和代碼分支。從這些記錄中,開發人員能看出對一個類的單元測試是否全面。關於測試和覆蓋,在11和12章還有更多的相關討論。
有些框架和代碼還經常會使用帶有額外屬性的專用(甚至用戶自定義的)類加載器。這些類加載器經常會在加載時對字節碼進行轉換,我們在第1章有提到過。
圖5-3中是類加載器的繼承層級以及不同加載器之間的相互關係。
圖5-3 類加載器層級
我們先來看一個專用類加載器的例子——如何用類加載實現依賴注入。
5.1.5 示例:依賴注入中的類加載器
DI有兩個核心思想。
- 系統內的功能單元要靠依賴項和配置信息才能正常發揮作用。
- 在對像自身的上下文裡,很難表示依賴項,即使可以,也很複雜難懂。
你腦海中應該浮現出一幅包含了行為,配置和依賴項信息(它們處在對像之外)的畫面。這部分通常被稱為對象的運行時路線。
第3章以Guice框架為例介紹了DI。本節中我們會討論利用類加載器實現DI的方式,但這種方式與Guice不同,實際上它更像簡化版的Spring框架。
在這個想像出來的DI框架下,我們像這樣啟動應用程序:
java-cp<CLASSPATH>org.framework.DIMain/path/to/config.xml
CLASSPATH中必須包含DI框架的JAR文件以及在config.xml文件中引用的所有類(以及它們的所有依賴項)的JAR文件。
我們改寫了前面一個類似的例子——代碼清單3-7中的服務,結果如清單5-1所示。
代碼清單5-1 HollywoodService
——不同的DI風格
public class HollywoodServiceDI {
private AgentFinder finder = null;
//空的構造方法
public HollywoodServiceDI {}
//setter方法
public void setFinder(AgentFinder finder) {
this.finder = finder;
}
public List<Agent>getFriendlyAgents {
...//同代碼清單3-7
}
public List<Agent>filterAgents(List<Agent>agents, String agentType) {
...//同代碼清單3-7
}
}
為了將它置於DI框架的管理之下,還需要一個配置文件:
<beans>
<bean
... />
<bean
p:finder-ref=\"agentFinder\"/>
</beans>
在這種方式中,DI框架利用配置文件來確定要構造的對象。這個例子需要hwService
和agentFinder
兩個bean,框架會為每個bean調用空構造方法,之後是setter方法(比如為HollywoodServiceDI
的依賴項AgentFinder
調用setFinder
)。
這說明類加載分為兩個階段。第一階段由應用類加載器完成,負責加載DIMain
及其引用的類。然後DIMain
開始運行,並在main
的參數中得到配置文件的位置。
這時候,框架已經在JVM中運行起來了,但config.xml中指定的用戶類還碰都沒碰呢。實際上,在DIMain
檢查配置文件之前,框架不可能知道要加載什麼類。
要啟動config.xml中指定的應用配置,需要類加載的第二階段。這要用到定制的類加載器。首先,要檢查config.xml文件的一致性,確保它沒有錯誤。然後,如果毫無差錯,定制的類加載器就會試圖從CLASSPATH
中加載指定類型。如果任何一步失敗了,整個過程就會被中止。
如果成功了,DI框架可以繼續創建所需的實例,並調用實例上的setter方法。如果這些都順利完成了,程序上下文就開始運行了。
我們簡單介紹了一下Spring風格的DI方式,其中大量使用了類加載。在Java技術中,還有很多要用到類加載器及其相關技術的領域。下面是一些眾所周知的例子:
- 插件架構;
- 廠商提供的或自主研發的框架;
- 從非正常位置(非文件系統或URL)獲取類文件;
- Java EE;
- 任何需要在JVM進程已經啟動後加入新的、未知代碼的情況下。
我們對類加載的討論就到此為止。讓我們進入下一節,探討Java 7為滿足反射等需求而提供的新API。