
所謂類(lèi)加載機(jī)制,就是虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存中,并對(duì)其進(jìn)行校驗(yàn),轉(zhuǎn)換,分析以及初始化,并最終形成虛擬機(jī)可以被使用java類(lèi)型的過(guò)程。
Java作為解釋型語(yǔ)言,支持動(dòng)態(tài)加載和動(dòng)態(tài)連接,類(lèi)型的加載、連接以及初始化過(guò)程都在程序運(yùn)行是完成,雖然這樣會(huì)導(dǎo)致類(lèi)加載的過(guò)程變慢,但是為Java語(yǔ)言提供了更好的靈活性,實(shí)現(xiàn)了動(dòng)態(tài)的擴(kuò)展。
1. 類(lèi)加載概述
1.1 類(lèi)的生命周期
類(lèi)從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載七個(gè)階段。
其中類(lèi)加載的過(guò)程包括了裝載、驗(yàn)證、準(zhǔn)備、解析、初始化五個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析三個(gè)步驟又合稱(chēng)為連接
類(lèi)加載的過(guò)程
在這五個(gè)階段中,加載、驗(yàn)證、準(zhǔn)備和初始化這四個(gè)階段發(fā)生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之后開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也成為動(dòng)態(tài)綁定或晚期綁定)。
這里簡(jiǎn)要說(shuō)明下Java中的綁定:綁定指的是把一個(gè)方法的調(diào)用與方法所在的類(lèi)(方法主體)關(guān)聯(lián)起來(lái),對(duì)java來(lái)說(shuō),綁定分為靜態(tài)綁定和動(dòng)態(tài)綁定:
- 靜態(tài)綁定:即前期綁定。在程序執(zhí)行前方法已經(jīng)被綁定,此時(shí)由編譯器或其它連接程序?qū)崿F(xiàn)。針對(duì)java,簡(jiǎn)單的可以理解為程序編譯期的綁定。java當(dāng)中的方法只有final,static,private和構(gòu)造方法是前期綁定的。
- 動(dòng)態(tài)綁定:即晚期綁定,也叫運(yùn)行時(shí)綁定。在運(yùn)行時(shí)根據(jù)具體對(duì)象的類(lèi)型進(jìn)行綁定。在java中,幾乎所有的方法都是后期綁定的。
1.2 類(lèi)文件從何而來(lái)
既然加載機(jī)制是虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存中的過(guò)程,那Class文件從何而來(lái)?
類(lèi)文件來(lái)源包括
- 從本地文件系統(tǒng)加載的class文件
- 從JAR包加載class文件
從網(wǎng)絡(luò)加載class文件 - 把一個(gè)Java源文件動(dòng)態(tài)編譯,并執(zhí)行加載
1.3 何時(shí)執(zhí)行類(lèi)的初始化
JVM規(guī)范中沒(méi)有明確說(shuō)明合適開(kāi)始類(lèi)的加載,但是指明一下情況下必須要對(duì)類(lèi)經(jīng)行初始化(加載、驗(yàn)證、準(zhǔn)備等階段自然要在這之前進(jìn)行):
- 創(chuàng)建類(lèi)實(shí)例。也就是new的方式;
- 調(diào)用某個(gè)類(lèi)的類(lèi)方法(靜態(tài)方法,invokeStatic指令碼);
- 訪問(wèn)某個(gè)類(lèi)或接口的類(lèi)變量(getStatic指令碼),或?yàn)樵擃?lèi)變量賦值(putStatic指令碼);
- 使用反射方式強(qiáng)制創(chuàng)建某個(gè)類(lèi)或接口對(duì)應(yīng)的java.lang.Class對(duì)象;
- 初始化某個(gè)類(lèi)的子類(lèi),則其父類(lèi)也會(huì)被初始化;
- 直接使用java.exe命令來(lái)運(yùn)行某個(gè)主類(lèi)(含有Main函數(shù));
2. 類(lèi)加載的過(guò)程
2.1 裝載
裝載是查找并加載類(lèi)的二進(jìn)制數(shù)據(jù)(查找和導(dǎo)入Class文件)的過(guò)程。作為類(lèi)加載過(guò)程的第一個(gè)階段,在裝載階段,JVM需要完成以下三件事情:
通過(guò)一個(gè)類(lèi)的全限定名來(lái)獲取其定義的二進(jìn)制字節(jié)流;
將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu);
在Java堆中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問(wèn)入口。
開(kāi)發(fā)人員既可以使用系統(tǒng)提供的類(lèi)加載器來(lái)完成加載,也可以自定義自己的類(lèi)加載器來(lái)完成加載。這部分內(nèi)容在后面的章節(jié)介紹。
2.2 連接
類(lèi)的加載過(guò)程后生成了類(lèi)的java.lang.Class對(duì)象,接著會(huì)進(jìn)入連接階段,連接階段負(fù)責(zé)將類(lèi)的二進(jìn)制數(shù)據(jù)合并入JRE(Java運(yùn)行時(shí)環(huán)境)中。類(lèi)的連接大致分三個(gè)階段。
- 驗(yàn)證:檢驗(yàn)被加載的類(lèi)是否有正確的內(nèi)部結(jié)構(gòu),并和其他類(lèi)協(xié)調(diào)一致;
- 準(zhǔn)備:負(fù)責(zé)為類(lèi)的類(lèi)變量分配內(nèi)存,并設(shè)置默認(rèn)初始值;
- 解析:將類(lèi)的二進(jìn)制數(shù)據(jù)中的符號(hào)引用替換成直接引用;
2.2.1 驗(yàn)證
驗(yàn)證的目的是確保被加載的類(lèi)的正確性
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。驗(yàn)證階段大致會(huì)完成4個(gè)階段的檢驗(yàn)動(dòng)作:
文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范;驗(yàn)證通過(guò)之后,裝載階段獲得字節(jié)流才會(huì)保存到方法區(qū);
元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析(注意:對(duì)比javac編譯階段的語(yǔ)義分析),以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求;例如:這個(gè)類(lèi)是否有父類(lèi),除了java.lang.Object之外。
字節(jié)碼驗(yàn)證:通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的、符合邏輯的。
符號(hào)引用驗(yàn)證:它發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候(解析階段中發(fā)生該轉(zhuǎn)化,后面會(huì)有講解),主要是對(duì)類(lèi)自身以外的信息(常量池中的各種符號(hào)引用)進(jìn)行匹配性的校驗(yàn)。
驗(yàn)證階段是非常重要的,但不是必須的,它對(duì)程序運(yùn)行期沒(méi)有影響,如果所引用的類(lèi)經(jīng)過(guò)反復(fù)驗(yàn)證,那么可以考慮采用-Xverifynone參數(shù)來(lái)關(guān)閉大部分的類(lèi)驗(yàn)證措施,以縮短虛擬機(jī)類(lèi)加載的時(shí)間。
2.2.2 準(zhǔn)備
準(zhǔn)備:為類(lèi)的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值。
準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配。對(duì)于該階段有以下幾點(diǎn)需要注意:
這時(shí)候進(jìn)行內(nèi)存分配的僅包括類(lèi)變量(static),而不包括實(shí)例變量,實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一塊分配在Java堆中。
這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類(lèi)型默認(rèn)的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
假設(shè)一個(gè)類(lèi)變量的定義為:public static int value = 3; 那么變量value在準(zhǔn)備階段過(guò)后的初始值為0,而不是3,因?yàn)檫@時(shí)候尚未開(kāi)始執(zhí)行任何Java方法,而把value賦值為3的putstatic指令是在程序編譯后,存放于類(lèi)構(gòu)造器<clinit>()方法之中的,所以把value賦值為3的動(dòng)作將在初始化階段才會(huì)執(zhí)行。

2.2.3 解析
解析:把類(lèi)中的符號(hào)引用轉(zhuǎn)換為直接引用。
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程,解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型、方法句柄和調(diào)用限定符7類(lèi)符號(hào)引用進(jìn)行。
- 符號(hào)引用就是一組符號(hào)來(lái)描述目標(biāo),可以是任何字面量;
- 直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄。
2.3 初始化
初始化,即對(duì)類(lèi)的靜態(tài)變量,靜態(tài)代碼塊執(zhí)行初始化操作。這是類(lèi)加載過(guò)程的最后一步,到了此階段,才真正開(kāi)始執(zhí)行類(lèi)中定義的Java程序代碼
初始化為類(lèi)的靜態(tài)變量賦予正確的初始值,在Java中對(duì)類(lèi)變量進(jìn)行初始值設(shè)定有兩種方式:
- 聲明類(lèi)變量是指定初始值。
- 使用靜態(tài)代碼塊為類(lèi)變量指定初始值。
類(lèi)的初始化步驟 / JVM初始化步驟:
如果這個(gè)類(lèi)還沒(méi)有被加載和鏈接,那先進(jìn)行加載和鏈接
假如這個(gè)類(lèi)存在直接父類(lèi),并且這個(gè)類(lèi)還沒(méi)有被初始化(注意:在一個(gè)類(lèi)加載器中,類(lèi)只能初始化一次),那就初始化直接的父類(lèi)(不適用于接口)
假如類(lèi)中存在初始化語(yǔ)句(如static變量和static塊),那就依次執(zhí)行這些初始化語(yǔ)句。
另一方面,初始化階段是執(zhí)行類(lèi)構(gòu)造器<clinit>()方法的過(guò)程:
-
<clinit>()方法是由編譯器自動(dòng)收集類(lèi)中的所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的,編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序所決定的; - JVM會(huì)保證每個(gè)類(lèi)的
<clinit>()都只執(zhí)行一遍,不會(huì)被反復(fù)加載; - JVM保證
<clinit>()執(zhí)行過(guò)程中的多線程安全;
3. 類(lèi)加載器
類(lèi)的加載器是Java語(yǔ)言的一種創(chuàng)新。
3.1 類(lèi)與類(lèi)加載器之間的關(guān)系
對(duì)于任意一個(gè)類(lèi),都需要由它的類(lèi)加載器和這個(gè)類(lèi)本身一同確定其在就Java虛擬機(jī)中的唯一性,也就是說(shuō),即使兩個(gè)類(lèi)來(lái)源于同一個(gè)Class文件,只要加載它們的類(lèi)加載器不同,那這兩個(gè)類(lèi)就必定不相等。這里的“相等”包括了代表類(lèi)的Class對(duì)象的equals()、isAssignableFrom()、isInstance()等方法的返回結(jié)果,也包括了使用instanceof關(guān)鍵字對(duì)對(duì)象所屬關(guān)系的判定結(jié)果。
3.2 類(lèi)加載器的分類(lèi)
站在Java虛擬機(jī)的角度來(lái)講,只存在兩種不同的類(lèi)加載器:
- 啟動(dòng)類(lèi)加載器:它使用C++實(shí)現(xiàn)(這里僅限于Hotspot,也就是JDK1.5之后默認(rèn)的虛擬機(jī),有很多其他的虛擬機(jī)是用Java語(yǔ)言實(shí)現(xiàn)的),是虛擬機(jī)自身的一部分。
- 所有其他的類(lèi)加載器:這些類(lèi)加載器都由Java語(yǔ)言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)之外,并且全部繼承自抽象類(lèi)java.lang.ClassLoader,這些類(lèi)加載器需要由啟動(dòng)類(lèi)加載器加載到內(nèi)存中之后才能去加載其他的類(lèi)。

站在Java開(kāi)發(fā)人員的角度來(lái)看,類(lèi)加載器可以大致劃分為以下三類(lèi):
- 啟動(dòng)類(lèi)加載器:Bootstrap ClassLoader,跟上面相同。它負(fù)責(zé)加載存放在
$JAVA_HOME/jre/lib/rt.jar里所有的class或-Xbootclassoath選項(xiàng)指定的jar包。由C++實(shí)現(xiàn),不是ClassLoader子類(lèi)。
啟動(dòng)類(lèi)加載器是無(wú)法被Java程序直接引用的。 - 擴(kuò)展類(lèi)加載器:Extension ClassLoader,該加載器由
sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包,比如$JAVA_HOME\jre\lib\ext目錄中,或者由java.ext.dirs系統(tǒng)變量指定的路徑中的所有類(lèi)庫(kù),開(kāi)發(fā)者可以直接使用擴(kuò)展類(lèi)加載器。 - 應(yīng)用程序類(lèi)加載器:Application ClassLoader,該類(lèi)加載器由
sun.misc.Launcher$AppClassLoader來(lái)實(shí)現(xiàn),它負(fù)責(zé)加載classpath中指定的jar包及Djava.class.path所指定目錄下的類(lèi)和jar包。開(kāi)發(fā)者可以直接使用該類(lèi)加載器,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。
3.3 雙親委派模型
應(yīng)用程序都是由以上三種類(lèi)加載器互相配合進(jìn)行加載的,如果有必要,我們還可以加入自定義的類(lèi)加載器。
加載器之間存在著層次關(guān)系,如下所示:

這種層次關(guān)系稱(chēng)為類(lèi)加載器的雙親委派模型。注意這里是以組合關(guān)系復(fù)用父類(lèi)加載器的父子關(guān)系,而不是以繼承關(guān)系實(shí)現(xiàn)的。
類(lèi)加載器的雙親委派加載機(jī)制:當(dāng)一個(gè)類(lèi)收到了類(lèi)加載請(qǐng)求,他首先不會(huì)嘗試自己去加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委派給父類(lèi)去完成,每一個(gè)層次類(lèi)加載器都是如此,因此所有的加載請(qǐng)求都應(yīng)該傳送到啟動(dòng)類(lèi)加載其中,只有當(dāng)父類(lèi)加載器反饋?zhàn)约簾o(wú)法完成這個(gè)請(qǐng)求的時(shí)候(在它的加載路徑下沒(méi)有找到所需加載的Class),子類(lèi)加載器才會(huì)嘗試自己去加載。
以下代碼可以驗(yàn)證類(lèi)加載器之間的父子層次關(guān)系
public class ClassLoaderTest {
public static void main(String[] args) {
//獲取系統(tǒng)/應(yīng)用類(lèi)加載器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系統(tǒng)/應(yīng)用類(lèi)加載器:" + appClassLoader);
//獲取系統(tǒng)/應(yīng)用類(lèi)加載器的父類(lèi)加載器,得到擴(kuò)展類(lèi)加載器
ClassLoader extcClassLoader = appClassLoader.getParent();
System.out.println("擴(kuò)展類(lèi)加載器" + extcClassLoader);
System.out.println("擴(kuò)展類(lèi)加載器的加載路徑:" + System.getProperty("java.ext.dirs"));
//獲取擴(kuò)展類(lèi)加載器的父加載器,但因根類(lèi)加載器并不是用Java實(shí)現(xiàn)的所以不能獲取
System.out.println("擴(kuò)展類(lèi)的父類(lèi)加載器:" + extcClassLoader.getParent());
}
}
輸出如下:
系統(tǒng)/應(yīng)用類(lèi)加載器:
sun.misc.Launcher$AppClassLoader@7f31245a
擴(kuò)展類(lèi)加載器sun.misc.Launcher$ExtClassLoader@45ee12a7
擴(kuò)展類(lèi)加載器的加載路徑:/Users/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
擴(kuò)展類(lèi)的父類(lèi)加載器:null
為什么根類(lèi)加載器為NULL?
根類(lèi)加載器并不是Java實(shí)現(xiàn)的,而且由于程序通常須訪問(wèn)根加載器,因此訪問(wèn)擴(kuò)展類(lèi)加載器的父類(lèi)加載器時(shí)返回NULL。
使用雙親委派模型來(lái)組織類(lèi)加載器之間的關(guān)系,有一個(gè)很明顯的好處,就是Java類(lèi)隨著它的類(lèi)加載器(說(shuō)白了,就是它所在的目錄)一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系,這對(duì)于保證Java程序的穩(wěn)定運(yùn)作很重要,保證同一個(gè)類(lèi)在不同的環(huán)境中都由同一個(gè)類(lèi)加載器來(lái)加載,保證一致性。
3.4 自定義類(lèi)加載器
JVM中除了根類(lèi)加載器之外的所有類(lèi)的加載器都是ClassLoader子類(lèi)的實(shí)例,通過(guò)重寫(xiě)ClassLoader中的方法,實(shí)現(xiàn)自定義的類(lèi)加載器
-
loadClass(String name,boolean resolve): 為ClassLoader的入口點(diǎn),根據(jù)指定名稱(chēng)來(lái)加載類(lèi),系統(tǒng)就是調(diào)用ClassLoader的該方法來(lái)獲取制定類(lèi)對(duì)應(yīng)的Class對(duì)象 -
findClass(String name):根據(jù)指定名稱(chēng)來(lái)查找類(lèi)
下面是實(shí)現(xiàn)findClass方法的自定義類(lèi)加載器的實(shí)例:
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
public class MyClassLoader extends ClassLoader {
// 讀取一個(gè)文件的內(nèi)容
@SuppressWarnings("resource")
private byte[] getBytes(String filename) throws IOException{
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int) len];
FileInputStream fin = new FileInputStream(file);
// 一次讀取class文件的全部二進(jìn)制數(shù)據(jù)
int r = fin.read(raw);
if (r != len)
throw new IOException("無(wú)法讀取全部文件" + r + "!=" + len);
fin.close();
return raw;
}
// 定義編譯指定java文件的方法
private boolean compile(String javaFile) throws IOException {
System.out.println("CompileClassLoader:正在編譯" + javaFile + "……..");
// 調(diào)用系統(tǒng)的javac命令
Process p = Runtime.getRuntime().exec("javac" + javaFile);
try {
// 其它線程都等待這個(gè)線程完成
p.waitFor();
} catch (InterruptedException ie) {
System.out.println(ie);
}
// 獲取javac 的線程的退出值
int ret = p.exitValue();
// 返回編譯是否成功
return ret == 0;
}
// 重寫(xiě)Classloader的findCLass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
// 將包路徑中的.替換成斜線/
String fileStub = name.replace(".", "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
// 當(dāng)指定Java源文件存在,且class文件不存在,或者Java源文件的修改時(shí)間比class文件//修改時(shí)間晚時(shí),重新編譯
if (javaFile.exists() && (!classFile.exists())
|| javaFile.lastModified() > classFile.lastModified()) {
try {
// 如果編譯失敗,或該Class文件不存在
if (!compile(javaFilename) || !classFile.exists()) {
throw new ClassNotFoundException("ClassNotFoundException:"
+ javaFilename);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
// 如果class文件存在,系統(tǒng)負(fù)責(zé)將該文件轉(zhuǎn)化成class對(duì)象
if (classFile.exists()) {
try {
// 將class文件的二進(jìn)制數(shù)據(jù)讀入數(shù)組
byte[] raw = getBytes(classFilename);
// 調(diào)用Classloader的defineClass方法將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成class對(duì)象
clazz = defineClass(name, raw, 0, raw.length);
} catch (IOException ie) {
ie.printStackTrace();
}
}
// 如果claszz為null,表明加載失敗,則拋出異常
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定義一個(gè)主方法
public static void main(String[] args) throws Exception {
// 如果運(yùn)行該程序時(shí)沒(méi)有參數(shù),即沒(méi)有目標(biāo)類(lèi)
if (args.length < 1) {
System.out.println("缺少運(yùn)行的目標(biāo)類(lèi),請(qǐng)按如下格式運(yùn)行java源文件:");
System.out.println("java CompileClassLoader ClassName");
}
// 第一個(gè)參數(shù)是需要運(yùn)行的類(lèi)
String progClass = args[0];
// 剩下的參數(shù)將作為運(yùn)行目標(biāo)類(lèi)時(shí)的參數(shù),所以將這些參數(shù)復(fù)制到一個(gè)新數(shù)組中
String progargs[] = new String[args.length - 1];
System.arraycopy(args, 1, progargs, 0, progargs.length);
MyClassLoader cl = new MyClassLoader();
// 加載需要運(yùn)行的類(lèi)
Class<?> clazz = cl.loadClass(progClass);
// 獲取需要運(yùn)行的類(lèi)的主方法
Method main = clazz.getMethod("main", (new String[0]).getClass());
Object argsArray[] = { progargs };
main.invoke(null, argsArray);
}
}
