在進入ClassLoader的分析之前我們先看一個JAVA程序例子。
class Singleton {
/* case 1 */
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
/**
* case 2
* public static int counter1 = 0;
* public static int counter2 = 0;
* private static Singleton singleton = new Singleton();
*/
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstance() {
return singleton;
}
}
public class MyTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("counter1 = " + singleton.counter1);
System.out.println("counter2 = " + singleton.counter2);
}
}
/**
* result in case 1:
* counter1 = 1
* counter2 = 0
* result in case 2:
* counter1 = 1
* counter2 = 1
*/
上面的代碼在case1 與case2 條件下運行結果卻不一樣,僅僅由於private static Singleton singleton = new Singleton();位置不同,要想瞭解其中的原因需要從類的使用時JVM完成的動作說起,在一個類被JVM使用時大致經歷如下三步(加載 — 鏈接(驗證–準備–解析) — 初始化)
那麼一個類被JVM使用時,必須預先經歷如上圖所述的加載過程。那麼什麼樣的條件才會觸發上述過程的執行呢?JAVA程序使用類分為主動使用和被動使用,在JVM的實現規范中要求,所有類的“主動使用“虛擬機才執行上述過程初始化相應的類,那麼問題就歸結為“主動使用”的意義。
1. 創建類的實例。Object A = new ClassA();
2. 訪問某個類或接口的靜態變量或對靜態變量賦值。如Class A{static a} 訪問A.a時。需要 指出的是訪問類的static final int x = 0(編譯時常量)並不被認為是類的主動使用,同樣 的假如有條件 Class A extends B;B{static a}如果使用A.a時隻會初始化類B,這種情況被認 為是對父類的主動使用。
3. 調用類的靜態方法
4.使用反射機制(Class.ForName(xxx)),而ClassLoader.load(並不會初始化類)
5. 初始化一個類的子類時,父類也被主動使用
6. 啟動類(java TestMain)
下面文章將針對上述過程給出比較詳細的說明。
加載過程
總的來說類的加載是JVM使用類加載器(如系統類加載器、擴展加載器、根加載器)在特定的加載路徑裡尋找class文件,並將class文件中的二進制數據讀入到內存中,其中class的數據結構被放置在運行時數據區的方法區類,並且在堆區裡創建該類的Class對象,用來封裝類的數據結構信息。其中類加載類的方式有:文件系統加載、網絡加載、zip jar 歸檔文件加載、數據庫中提取、動態編譯的源文件加載。
類加載的最終產品是位於堆區中的Class對象,其封裝瞭類在方法區內數據結構,並且向Java程序員提供瞭訪問方法區內數據結構的接口,需要指出的是,類的加載並不都是主動使用時才加載,加載器可以實現為有預加載功能,如使用一定的算法預測類的使用。在上面的敘述中我們提到過JVM使用類加載器對class文件進行加載(本文後面部分將著重描述類的加載機制)。
連接過程
類加載後,就是連接階段瞭,連接就是將已經讀入到內存的類的二進制數據合並到虛擬機的運行環境中去。連接的第一個階段是類的驗證,驗證的內容如下:
1.類文件結構的檢查,確保類的文件遵循java類文件的固定格式。
2. 語義檢查:確保類本身符合java語言的語法規定,比如驗證final類型沒有子 類,final方法沒有被從寫,private沒有被重寫。
3. 字節碼驗證:確定字節碼流可以被java虛擬機安全的執行。
4.二進制兼容驗證:確保相互應用的類之間協調一致。
做完驗證之後就是類的準備階段,完成的工作為類的靜態變量分配內存並設置為初始值。如有類
Sample{
Static int a = 1;
Static long b;
}
系統會為 a 分配4個字節,並設置初始值為 0,為b分配8個字節並設置初始值為 0.
做完類的準備工作之後就是類的解析,主要工作就是把類中的二進制中的符號引用替換為直接引用。我們舉個例子 void show{ objectA.print()};objectA.print() 就是對ClassA的一個符號引用,經過解析之後該處的代碼會被一個指向ClassA中方法區print方法的指針。
初始化過程
下面是類的初始化過程,初始化的主要步驟為:檢查該類是否已經被加載和連接;如果該類有父類,且沒有初始化,對父類進行加載 連接 初始化;假如類中存在初始化語句,依次執行初始化語句。而靜態變量的聲明以及靜態代碼塊都被看作是類的初始化語句,java虛擬機會按照初始化語句在類文件中的順序依次來執行他們。如 static int a =1,與static{ a = 3}這樣的語句都會被JVM順序執行。
前面提到,當JVM初始化一個類時要求他的父類已經被初始化,這樣的機制並不適用於接口,初始化一個類時,它實現的接口並不需要被初始化,初始化一個接口時,其父接口也不需要被初始化,隻有當程序首次使用接口中的靜態變量時,才會導致接口的初始化。
通過上面的論述,我們大致對類的使用有瞭一個初步的瞭解,接下來我們將分析本文開始時提出的那個程序的運行結果
在case 1 中,在MyTest那種使用 Singleton singleton = Singleton.getInstance();這樣的語句為類的主動使用這會觸發Singleton類的加載 連接 初始化。
private static Singleton singleton = new Singleton();(A)
public static int counter1;(B)
public static int counter2 = 0;(C)
在加載完後的連接階段的準備期,會為singleton分配內存,設定默認值為null,counter1默認值為 0,counter2 默認值為 0. 進入初始化階段,第一步為singleton賦值 會調用Singleton的構造方法,此時執行counter1++,counter2++ counter1 = 1,counter2 = 1;第二步為counter1賦值,由於沒有賦值語句counter1 仍為1;第三步為 counter2賦值,counter2 被賦值為 0,所以結果是counter1 =1
Counter2 =0
在case2 中 使用如下語句
public static int counter1;(A)
public static int counter2 = 0;(B)
private static Singleton singleton = new Singleton();(C)
連接準備階段結束後counter1 = 0,counter2 = 0,singleton = null;初始化時 第一步A語句不用初始化 counter1 =0;第二部B語句初始化為0,counter2 =0;第三步 調用構造函數 counter1++,counter2++ counter1 = 1,counter2 = 1;所以case2的結果為 counter1 =1 counter2 = 1
在下面的部分,我將給大傢深入的介紹一下JVM的類加載器。
類加載器分析
在上面的例子中我們看到一個類的主動使用會經歷加載、鏈接、初始化的過程,類的加載需要使用JVM的類加載器,JVM的類加載器使用父親委托機制。我們首先看看JVM的類加載器樹形結構:
BootStrap(根類加載器)
||
Extend(擴展類加載器)
||
System(系統類或應用類加載器)
||
用戶自定義類加載器
從上面的結構可以看出,JVM的類加載器一共有四大類,其中根類加載器、擴展類加載器、系統類加載器(應用類加載器)為JVM自帶的類加載器。
1.根類加載器。負責加載虛擬機的核心類庫,如java.lang.*等。根類加載器從系統 屬性sun.boot.class.path 所指定的目錄中加載類庫。根類加載器的實現依賴於底層 操作系統,屬於jvm實現的一部分,使用c++語言編寫。它並沒有ClassLoader類, 也沒有父加載器。使用如stringObject.getClass().getClassLoader()將返null。
2. 擴展類加載器(Ext)。從圖中可以看出它的父加載器是根加載器。它 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或者從jdk的安裝目錄 的jre\lib\ext子目錄下加載類庫,如果你把用戶創建的jar放在這個目錄下會被擴展類 加載器加載。擴展類加載器使用純java實現,繼承瞭ClassLoader。
3. 系統類加載器,也叫應用類加載器(APP),它的父加載器是Ext加載器。它從 環境 變量裡(安裝JDK時設立的classpath)或從系統屬性java.class.path加載 類,它是 用戶自定義的類加載器的默認父加載器,采用純java實現,繼承 自ClassLoader。
4. 用戶自定義加載器。系統類加載器的子類,必須要繼承自ClassLoader之類,並 且重 寫findClass方法。
假設我們自定義ClassLoader 為loadA,並且使用loadA.load(Class),所謂的父親委托機制就是loadA 委托父加載器(App)加載Class,App委托Ext,Ext委托BootStrap,如果BootStrap不能加載,則讓Ext加載,逐級下發,如果直到loadA還不能加載Class這拋出ClassNotFindException。委托機制是SUN公司基於安全性考慮的,這樣可以保證Object這樣的重要類隻能有JVM加載。我們定義若一個類加載器能夠成功加載類Class,我們則稱這個加載器為該類的定義加載器,其下的子加載器為初始化加載器。如在上述的類之中,假設App類加載器加載瞭類Class 這App為定義加載器,APP與LoadA為初始化加載器。
需要指出的是加載器之間的父子關系並不是指類之間的繼承關系,而是指加載器之間的包裝關系。一對父子可能是同一個類加載器的實例,也可能不是。例如我們自定義類加載器MyClassLoader。LoadA = new MyClassLoader(); LoadB = new MyClassLoader(loadA), 我們稱loadB包裝瞭loadA,LoadA是loadB的父加載器。
運行時包決定瞭protecetd類和protected成員是否能夠訪問。我們知道所有的protected的成員需要同一個包下的類才能訪問。如果我們定義java.lang.Spy類,我們是否就能訪問java.lang.*下的核心protected資源呢?運行時包包括,包名相同,類加載器相同,所有java.lang.Spy 與java.lang.* 不在相同的運行時包,答案是否定的。
下面我們以視頻中一個詳細的例子來講述用戶自定義加載器的實現。首先給出例子中類加載器的樹形關:
BootStrap(根類加載器)
|| \\
|| LoaderC —–> d:app/otherlib
Extend(擴展類加載器)
||
System(系統類或應用類加載器)
||
LoaderB ——> d:app/serverlib
||
LoaderA ——> d:app/clientlib
在例子中我們定義三個類加載器,在重寫的findClass方法中設定好加載路徑。JDK API給出一個自定義ClassLoad的方法:
class MyClassLoader extends ClassLoader {
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}
從上面的代碼可以看出通過重寫findClass方法並在loadClassData中設定好加載.class 文件的路徑可以實現自定義的加載機制。
假設我們已經定義好自己的加載器MyClassLoader,我們使用如下的代碼便能夠構造出例子中的類加載器樹形結構。
MyClassLoader loadB = new MyClassLoader(“loadB”);
loadB.setPath(“D://app//serverlib”);
MyClassLoader loadA = new MyClassLoader(loadB,“loadA”);
loadB.setPath(“D://app//cientlib”);
MyClassLoader loadC = new MyClassLoader(null,”loadC”)//null代表父加載器為Bootstrap
loadC.setPath(“D://app//otherlib”);
我們一個測試類Sample 和一個測試類Dog進行加載測試。
Class Sample{
Static{
New Dog();
}
}
Class文件的存放路徑如下:
d:app/syslib/MyClassLoader.class
d:app/serverlib/Sample.class
d:app/clientlib/Dog.class
d:app/otherlib/Sample.class,Dog.class
TestCase1 :
Class clazz = loadA.loadClass(“Sample”);
Clazz.newInstance();
TestCase2 :
Class clazz = loadB.loadClass(“Sample”);
Clazz.newInstance();
TestCase3 :
Class clazz = loadC.loadClass(“Sample”);
Clazz.newInstance();
在case1 中 由加載器的樹形結構可以看出:loadA加載Sample時委托父親LoadB加載Sample,由於再向上委托並不能加載Sample,所以 Sample 由LoadB在 app/serverlib 下加載,
對於Dog類,LoadA的所有父加載器都不能加載,所以有loadA在 app/clientlib下加載
在case2 中 有loadB在 app/serverlib中加載 Sample,由於loadB 與其父加載器都不能加載Dog,所以會拋出ClassNotfoundException。此時如果把Dog拷貝到 syslib下,Dog類就會被appCloader加載,而不會出現ClassNotFound錯誤。
在Case3 中LoadC直接委托Bootstrap加載Sample,由於無法加載隻能有自己加載,所以Sample 與Dog都會從 app/otherlib/下加載.
假設 我們在MyClassLoader中寫意給main方法測試
TestCase4 :
Class clazz = loadA.loadClass(“Sample”);
Sample sample = Clazz.newInstance();
此時會導致一個NoClassDefError,這主要是JVM的類加載器命名空間規則導致的,在jvm中子加載器的命令空間包含瞭父加載器加載的所有類,反過來則不成立,因為MyClassLoader類是有appLoader加載的,所以其看不見有LoadB與loadA加載的類。
在這裡順便提一下,在一個類主動使用時,該類就開始起生命周期 加載,鏈接,初始化,使用,卸載。Jvm自帶的加載器加載的Class是不能夠被卸載的,隻有用戶自定義加載器加載的類才能被卸載,卸載機制是根據對類的引用計數情況而定,這與GC根據引用情況回收垃圾差不多