引文
java.lang.ClassNotFoundException
異常(方法 loadClass()
拋出);java.lang.NoClassDefFoundError
異常(方法 defineClass()
拋出)java.lang.ClassCastException
異常類加載器的樹狀組織結構
Java 中的類加載器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。系統提供的類加載器主要有下面三個:
- 引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來
-
實現的,並不繼承自
java.lang.ClassLoader
。 - 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
- 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應用的類都是由它來完成加載的。可以通過
-
ClassLoader.getSystemClassLoader()
來獲取它。
除了系統提供的類加載器以外,開發人員可以通過繼承 java.lang.ClassLoader
類的方式實現自己的類加載器,以滿足一些特殊的需求。
除了引導類加載器之外,所有的類加載器都有一個父類加載器。通過 表
1中給出的 getParent()
方法可以得到。對於系統提供的類加載器來說,系統類加載器的父類加載器是擴展類加載器,而擴展類加載器的父類加載器是引導類加載器;對於開發人員編寫的類加載器來說,其父類加載器是加載此類加載器
Java 類的類加載器。因爲類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。圖
1中給出了一個典型的類加載器樹狀組織結構示意圖,其中的箭頭指向的是父類加載器。
圖 1. 類加載器樹狀組織結構示意圖
類裝入器首先判斷要求它裝入的類是否與過去裝入的類相同。如果相同,就返回上次返回的類(即保存在緩存中的類)。如果不是,就把裝入類的機會交給父類。這兩步遞歸地以深度優先的方式重複。如果父類返回 null(或拋出 ClassNotFoundException
),那麼類裝入器會在自己的路徑中尋找類的源。
因爲父類類裝入器總是先得到裝入類的機會,所以類裝入器裝入的類最靠近根。這意味着所有核心引導類都是由引導裝入器裝入的,這就保證裝入了類(例如 java.lang.Object
)的正確版本。這也可以讓類裝入器看到自己或父類或祖先裝入的類,但是不能看到子女裝入的類。
圖 1 顯示了三個標準的類裝入器及裝載路徑:
圖 1. 類裝入器委託模型
類加載器的代理模式
類加載器與 Web 容器()
絕大多數情況下,Web 應用的開發人員不需要考慮與類加載器相關的細節。下面給出幾條簡單的原則:
-
每個 Web 應用自己的 Java 類文件和使用的庫的 jar 包,分別放在
WEB-INF/classes
和WEB-INF/lib
目錄下面。
- 多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面
- 。
- 當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。
下面介紹一種加載類的方法:Class.forName
。
Class.forName
Class.forName
是一個靜態方法,同樣可以用來加載類。該方法有兩種形式:Class.forName(String
name, boolean initialize, ClassLoader loader)
和 Class.forName(String
className)
。第一種形式的參數 name
表示的是類的全名;initialize
表示是否初始化類;loader
表示加載時使用的類加載器。第二種形式則相當於設置了參數 initialize
的值爲 true
,loader
的值爲當前類的類加載器。Class.forName
的一個很常見的用法是在加載數據庫驅動的時候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用來加載
Apache Derby 數據庫的驅動。
- Class.forName("com.mysql.jdbc.Driver");
- String url = "jdbc:mysql://127.0.0.1/test?useUnicode=true&characterEncoding=utf-8";
- String user = "";
- String psw = "";
- Connection con = DriverManager.getConnection(url,user,psw);
sun.misc.Launcher$AppClassLoader
類的實例;第二個輸出的是擴展類加載器,是 sun.misc.Launcher$ExtClassLoader
類的實例。需要注意的是這裏並沒有輸出引導類加載器,這是由於有些
JDK 的實現對於父類加載器是引導類加載器的情況,getParent()
方法返回 null
。類裝入的階段
類的裝入實際上可以分成三個階段:裝入、鏈接和初始化。
雖然不是所有的問題,但至少大多數與類裝入有關的問題都可以追溯到在這三個階段中發生的某個問題。所以,對於每一階段的深入理解有助於對類裝入問題的診斷。圖 2 顯示了這三個階段:
圖 2. 類裝入的階段
裝入 階段包括:找到必要的類(通過查找每個類路徑)並裝入字節碼。在 JVM 中,裝入階段爲類對象提供了非常基本的內存結構。在這一階段不處理方法、字段和引用的其他類。所以,類還不能使用。
鏈接 是三個階段中最複雜的一個。可以把它分成三個主要階段:
- 字節碼驗證。 類裝入器對於類的字節碼要做許多檢測,以確保格式正確、行爲正確。
- 類準備。 這個階段準備代表每個類中定義的字段、方法和實現接口所必需的數據結構。
-
解析。 在這個階段,類裝入器裝入類所引用的其他所有類。可以用許多方式引用類:
- 超類
- 接口
- 字段
- 方法簽名
- 方法中使用的本地變量
在初始化 階段,類中包含的靜態初始化器都被執行。在這一階段末尾,靜態字段被初始化成默認值。
在這三個階段末尾,類被完整地裝入,可以使用了。請注意可以用惰性方式執行類裝入,所以類裝入過程的某些部分可能在第一次使用類的時候才執行,而不是在裝入時執行。
顯式裝入與隱式裝入
類裝入的方式有兩種 —— 顯式 或 隱式,兩者之間有些細微差異。
顯式 類裝入發生在使用以下方法調用裝入的類的時候:
-
cl.loadClass()
(cl
是java.lang.ClassLoader
的實例) -
Class.forName()
(啓動的類裝入器是當前類定義的類裝入器)
當調用其中一個方法的時候,指定的類(以類名爲參數)由類裝入器裝入。如果類已經裝入,那麼只是返回一個引用;否則,裝入器會通過委託模型裝入類。
隱式 類裝入發生在由於引用、實例化或繼承導致裝入類的時候(不是通過顯式方法調用)。在每種情況下,裝入都是在幕後啓動的,JVM 會解析必要的引用並裝入類。與顯式類裝入一樣,如果類已經裝入了,那麼只是返回一個引用;否則,裝入器會通過委託模型裝入類。
類的裝入通常組合了顯式和隱式類裝入。例如,類裝入器可能先顯式地裝入一個類,然後再隱式地裝入它引用的所有類