類加載的機制的層次結(jié)構(gòu)
每個編寫的”.java”拓展名類文件都存儲著需要執(zhí)行的程序邏輯,這些”.java”文件經(jīng)過Java編譯器編譯成拓展名為”.class”的文件,”.class”文件中保存著Java代碼經(jīng)轉(zhuǎn)換后的虛擬機指令,當需要使用某個類時,虛擬機將會加載它的”.class”文件,并創(chuàng)建對應(yīng)的class對象,將class文件加載到虛擬機的內(nèi)存,這個過程稱為類加載,這里我們需要了解一下類加載的過程,如下:

加載:類加載過程的一個階段:通過一個類的完全限定查找此類字節(jié)碼文件,并利用字節(jié)碼文件創(chuàng)建一個Class對象
驗證:目的在于確保Class文件的字節(jié)流中包含信息符合當前虛擬機要求,不會危害虛擬機自身安全。主要包括四種驗證,文件格式驗證,元數(shù)據(jù)驗證,字節(jié)碼驗證,符號引用驗證。
準備:為類變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類變量的初始值即0(如static int i=5;這里只將i初始化為0,至于5的值將在初始化時賦值),這里不包含用final修飾的static,因為final在編譯的時候就會分配了,注意這里不會為實例變量分配初始化,類變量會分配在方法區(qū)中,而實例變量是會隨著對象一起分配到Java堆中。
解析:主要將常量池中的符號引用替換為直接引用的過程。符號引用就是一組符號來描述目標,可以是任何字面量,而直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。有類或接口的解析,字段解析,類方法解析,接口方法解析(這里涉及到字節(jié)碼變量的引用,如需更詳細了解,可參考《深入Java虛擬機》)。
初始化:類加載最后階段,若該類具有超類,則對其進行初始化,執(zhí)行靜態(tài)初始化器和靜態(tài)初始化成員變量(如前面只初始化了默認值的static變量將會在這個階段賦值,成員變量也將被初始化)。
這便是類加載的5個過程,而類加載器的任務(wù)是根據(jù)一個類的全限定名來讀取此類的二進制字節(jié)流到JVM中,然后轉(zhuǎn)換為一個與目標類對應(yīng)的java.lang.Class對象實例,在虛擬機提供了3種類加載器,引導(Bootstrap)類加載器、擴展(Extension)類加載器、系統(tǒng)(System)類加載器(也稱應(yīng)用類加載器)
ClassLoader加載機制如下:

啟動(Bootstrap)類加載器
啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實現(xiàn)的,是虛擬機自身的一部分,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中,注意必由于虛擬機是按照文件名識別加載jar包的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類)。
擴展(Extension)類加載器
擴展類加載器是指Sun公司(已被Oracle收購)實現(xiàn)的sun.misc.Launcher$ExtClassLoader類,由Java語言實現(xiàn)的,是Launcher的靜態(tài)內(nèi)部類,它負責加載<JAVA_HOME>/lib/ext目錄下或者由系統(tǒng)變量-Djava.ext.dir指定位路徑中的類庫,開發(fā)者可以直接使用標準擴展類加載器。
//ExtClassLoader類中獲取路徑的代碼
private static File[] getExtDirs() {
//加載<JAVA_HOME>/lib/ext目錄中的類庫
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
系統(tǒng)(System)類加載器
也稱應(yīng)用程序加載器是指 Sun公司實現(xiàn)的sun.misc.Launcher$AppClassLoader。它負責加載系統(tǒng)類路徑j(luò)ava -classpath或-D java.class.path 指定路徑下的類庫,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用系統(tǒng)類加載器,一般情況下該類加載是程序中默認的類加載器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器。
在Java的日常應(yīng)用程序開發(fā)中,類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時,我們還可以自定義類加載器,需要注意的是,Java虛擬機對class文件采用的是按需加載的方式,也就是說當需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象,而且加載某個類的class文件時,Java虛擬機采用的是雙親委派模式即把請求交由父類處理,它一種任務(wù)委派模式,下面我們進一步了解它。
理解雙親委派模式
雙親委派模式工作原理
雙親委派模式要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當有自己的父類加載器,請注意雙親委派模式中的父子關(guān)系并非通常所說的類繼承關(guān)系,而是采用組合關(guān)系來復用父類加載器的相關(guān)代碼,類加載器間的關(guān)系如下:

雙親委派模式是在Java 1.2后引入的,其工作原理的是,如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執(zhí)行,如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的啟動類加載器,如果父類加載器可以完成類加載任務(wù),就成功返回,倘若父類加載器無法完成此加載任務(wù),子加載器才會嘗試自己去加載,這就是雙親委派模式。
雙親委派模式優(yōu)勢
類加載器的雙親委托模型好處:
1、可以確保Java核心庫的類型安全:所有的Java應(yīng)用都至少會引用java.lang.Object類,也就是在運行期java.lang.Object類會加載到Java虛擬機中,如果這個加載過程是由Java自己的類加載器所完成,那么就很可能會在JVM中存在多個版本的java.lang.Object類,而且這些類還不兼容,并且互相不可見(正是命名空間在發(fā)揮作用),借助于雙親委托機制,Java核心類庫中的類的加載工作,都是由啟動類加載器來統(tǒng)一完成加載工作,從而確保了Java應(yīng)用使用的都是同一個版本的Java核心類庫,它們之間是相互兼容的。
2、可以確保Java核心類庫所提供的類,不會被自定義的類所替代。
3、不同的類加載器可以為相同名稱(binary name)的類創(chuàng)建額外的命名空間,相同名稱的類可以并存在Java虛擬機中,只需要不同的類加載器來加載它們即可,不同類加載器所加載的類之間是不兼容的,這就相當于在Java虛擬機內(nèi)部創(chuàng)建了一個又一個項目隔離的Java類空間,這類技術(shù)在很多框架中都得到了實際應(yīng)用。
4、采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層級關(guān)可以避免類的重復加載,當父親已經(jīng)加載了該類時,就沒有必要子ClassLoader再加載一次。
Bootstrap啟動類加載器作用:
在運行期,一個Java類是由該類的完全限定名(binary name,二進制名)和用于加載該類的定義類加載器(defining loader)所共同決定的,如果同樣名字(即相同的全限定名)的類是由兩個不同的加載器所加載,那么這些類就是不同的,即便.class文件的字節(jié)碼完全一樣,并且從相同的位置加載亦是如此。
在Oracle的Hotspot實現(xiàn)中,系統(tǒng)屬性sun.boot.class.path如果修改錯了,則運行出錯。
內(nèi)建于JVM中的啟動類加載器會加載java.lang.ClassLoader以及其他的Java平臺類,當JVM啟動時,一塊特殊的機器碼會運行,它會加載擴展類加載器與系統(tǒng)類加載器,這塊特殊的機器碼叫做啟動類加載器(Bootstrap),啟動類加載器并不是Java類,而其他的加載器是Java類,啟動類加載器是特于平臺的機器指令,它負責開啟整個加載過程
所有類加載器(除了啟動類加載器)都被實現(xiàn)為Java類,不過總歸要有一個組件來加載第一個Java類加載器,從而讓整個加載過程能夠順利進行下去,加載第一個純Java類加載器就是啟動類加載器的職責
啟動類加載器還會負責加載供JRE正常運行所需要的基本組件,即rt包下的類,包括java.util和java.lang包等
下面代碼可以看到各類加載器的加載路徑:
public class MyTest22 {
public static void main(String[] args) {
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
System.out.println(ClassLoader.class.getClassLoader());
System.out.println(Launcher.class.getClassLoader());
}
}
下面我們從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實現(xiàn),它們類圖關(guān)系如下

從圖可以看出頂層的類加載器是ClassLoader類,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器),這里我們主要介紹ClassLoader中幾個比較重要的方法。
-
loadClass(String name, boolean resolve)
該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2之后不再建議用戶重寫但用戶可以直接調(diào)用該方法,loadClass()方法是ClassLoader類自己實現(xiàn)的,該方法中的邏輯就是雙親委派模式的實現(xiàn),其源碼如下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數(shù)代表是否生成class對象的同時進行解析相關(guān)操作。:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// // 先從緩存查找該class對象,找到就不用重新加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到,則委托給父類加載器去加載
c = parent.loadClass(name, false);
} else {
//如果沒有父類,則委托給啟動加載器去加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 如果都沒有找到,則通過自定義實現(xiàn)的findClass去查找并加載
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//是否需要在加載時進行解析
resolveClass(c);
}
return c;
}
}
正如loadClass方法所展示的,當類加載請求到來時,先從緩存中查找該類對象,如果存在直接返回,如果不存在則交給該類加載器的父加載器去加載,倘若沒有父加載器則交給頂級啟動類加載器去加載,最后倘若仍沒有找到,則使用findClass()方法去加載(關(guān)于findClass()稍后會進一步介紹)。從loadClass實現(xiàn)也可以知道如果不想重新定義加載類的規(guī)則,也沒有復雜的邏輯,只想在運行時加載自己指定的類,那么我們可以直接使用this.getClass().getClassLoder.loadClass("className"),這樣就可以直接調(diào)用ClassLoader的loadClass方法獲取到class對象。
-
findClass(String name)
在JDK1.2之前,在自定義類加載時,總會去繼承ClassLoader類并重寫loadClass方法,從而實現(xiàn)自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調(diào)用的,當loadClass()方法中父加載器加載失敗后,則會調(diào)用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模式。需要注意的是ClassLoader類中并沒有實現(xiàn)findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應(yīng)該知道的是findClass方法通常是和defineClass方法一起使用的(稍后會分析),ClassLoader類中findClass()方法源碼如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
//直接拋出異常
throw new ClassNotFoundException(name);
}
-
defineClass(byte[] b, int off, int len)
defineClass()方法是用來將byte字節(jié)流解析成JVM能夠識別的Class對象(ClassLoader中已實現(xiàn)該方法邏輯),通過這個方法不僅能夠通過class文件實例化class對象,也可以通過其他方式實例化class對象,如通過網(wǎng)絡(luò)接收一個類的字節(jié)碼,然后轉(zhuǎn)換為byte字節(jié)流創(chuàng)建對應(yīng)的Class對象,defineClass()方法通常與findClass()方法一起使用,一般情況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法并編寫加載規(guī)則,取得要加載類的字節(jié)碼后轉(zhuǎn)換成流,然后調(diào)用defineClass()方法生成類的Class對象,簡單例子如下:
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
// 獲取類的字節(jié)數(shù)組
byte[] data = this.loadClassData(className);
if(data == null){
throw new ClassNotFoundException();
}
//使用defineClass生成class對象
return this.defineClass(className,data,0,data.length);
}
}
需要注意的是,如果直接調(diào)用defineClass()方法生成類的Class對象,這個類的Class對象并沒有解析(也可以理解為鏈接階段,畢竟解析是鏈接的最后一步),其解析操作需要等待初始化階段進行。
-
resolveClass(Class<?> c)
使用該方法可以使用類的Class對象創(chuàng)建完成也同時被解析。前面我們說鏈接階段主要是對字節(jié)碼進行驗證,為類變量分配內(nèi)存并設(shè)置初始值同時將字節(jié)碼文件中的符號引用轉(zhuǎn)換為直接引用。
上述4個方法是ClassLoader類中的比較重要的方法,也是我們可能會經(jīng)常用到的方法。接看SercureClassLoader擴展了 ClassLoader,新增了幾個與使用相關(guān)的代碼源(對代碼源的位置及其證書的驗證)和權(quán)限定義類驗證(主要指對class源碼的訪問權(quán)限)的方法,一般我們不會直接跟這個類打交道,更多是與它的子類URLClassLoader有所關(guān)聯(lián),前面說過,ClassLoader是一個抽象類,很多方法是空的沒有實現(xiàn),比如 findClass()、findResource()等。而URLClassLoader這個實現(xiàn)類為這些方法提供了具體的實現(xiàn),并新增了URLClassPath類協(xié)助取得Class字節(jié)碼流等功能,在編寫自定義類加載器時,如果沒有太過于復雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節(jié)碼流的方式,使自定義類加載器編寫更加簡潔,下面是URLClassLoader的類圖(利用IDEA生成的類圖)

從類圖結(jié)構(gòu)看出URLClassLoader中存在一個URLClassPath類,通過這個類就可以找到要加載的字節(jié)碼流,也就是說URLClassPath類負責找到要加載的字節(jié)碼,再讀取成字節(jié)流,最后通過defineClass()方法創(chuàng)建類的Class對象。
從URLClassLoader類的結(jié)構(gòu)圖可以看出其構(gòu)造方法都有一個必須傳遞的參數(shù)URL[],該參數(shù)的元素是代表字節(jié)碼文件的路徑,換句話說在創(chuàng)建URLClassLoader對象時必須要指定這個類加載器的到那個目錄下找class文件。
同時也應(yīng)該注意URL[]也是URLClassPath類的必傳參數(shù),在創(chuàng)建URLClassPath對象時,會根據(jù)傳遞過來的URL數(shù)組中的路徑判斷是文件還是jar包,然后根據(jù)不同的路徑創(chuàng)建FileLoader或者JarLoader或默認Loader類去加載相應(yīng)路徑下的class文件,而當JVM調(diào)用findClass()方法時,就由這3個加載器中的一個將class文件的字節(jié)碼流加載到內(nèi)存中,最后利用字節(jié)碼流創(chuàng)建類的class對象。
請記住,如果我們在定義類加載器時選擇繼承ClassLoader類而非URLClassLoader,必須手動編寫findclass()方法的加載邏輯以及獲取字節(jié)碼流的邏輯。
了解完URLClassLoader后接著看看剩余的兩個類加載器,即拓展類加載器ExtClassLoader和系統(tǒng)類加載器AppClassLoader,這兩個類都繼承自URLClassLoader,是sun.misc.Launcher的靜態(tài)內(nèi)部類。sun.misc.Launcher主要被系統(tǒng)用于啟動主應(yīng)用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher創(chuàng)建的,其類主要類結(jié)構(gòu)如下:


它們間的關(guān)系正如前面所闡述的那樣,同時我們發(fā)現(xiàn)ExtClassLoader并沒有重寫loadClass()方法,這足矣說明其遵循雙親委派模式,而AppClassLoader重載了loadCass()方法,但最終調(diào)用的還是父類loadClass()方法,因此依然遵守雙親委派模式,重載方法源碼如下:
/**
* Override loadClass 方法,新增包權(quán)限檢測功能
*/
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
//依然調(diào)用父類的方法
return super.loadClass(var1, var2);
}
}
其實無論是ExtClassLoader還是AppClassLoader都繼承URLClassLoader類,因此它們都遵守雙親委托模型,這點是毋庸置疑的。
到此我們對ClassLoader、URLClassLoader、ExtClassLoader、AppClassLoader以及Launcher類間的關(guān)系有了比較清晰的了解,同時對一些主要的方法也有一定的認識,這里并沒有對這些類的源碼進行詳細的分析,畢竟沒有那個必要,因為我們主要弄得類與類間的關(guān)系和常用的方法同時搞清楚雙親委托模式的實現(xiàn)過程,為編寫自定義類加載器做鋪墊就足夠了。前面出現(xiàn)了很多父類加載器的說法,但每個類加載器的父類到底是誰,一直沒有闡明,下面我們就通過代碼驗證的方式來闡明這答案。
類加載器間的關(guān)系
我們進一步了解類加載器間的關(guān)系(并非指繼承關(guān)系),主要可以分為以下4點:
- 啟動類加載器,由C++實現(xiàn),沒有父類。
- 拓展類加載器(ExtClassLoader),由Java語言實現(xiàn),父類加載器為null
- 系統(tǒng)類加載器(AppClassLoader),由Java語言實現(xiàn),父類加載器為ExtClassLoader
- 自定義類加載器,父類加載器肯定為AppClassLoader。
下面我們通過程序來驗證上述闡述的觀點
public class MyClassLoader extends ClassLoader {
private String classLoaderName;
private String path;
private final String fileExtension = ".class";
public MyClassLoader(String classLoaderName){
super();//將系統(tǒng)類加載器作為該類加載器的父加載器
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader parent,String classLoaderName){
super(parent);//顯示指定該類加載器的父加載器
this.classLoaderName = classLoaderName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "MyTest15{" +
"classLoaderName='" + classLoaderName + '\'' +
'}';
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("findClass invoked:"+className);
System.out.println("class loader name:"+this.classLoaderName);
byte[] data = this.loadClassData(className);
if(data == null){
throw new ClassNotFoundException();
}
return this.defineClass(className,data,0,data.length);
}
private byte[] loadClassData(String className){
InputStream inputStream = null;
byte[] data = null;
ByteArrayOutputStream bos = null;
try {
this.path = path.replace(".","/");
this.classLoaderName = classLoaderName.replace(".","/");
inputStream = new FileInputStream(new File(this.path + className + this.classLoaderName));
bos = new ByteArrayOutputStream();
int ch;
while((ch = inputStream.read()) != -1){
bos.write(ch);
}
data = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("loader1");
System.out.println("自定義類加載器的父加載器: "+loader1.getParent());
System.out.println("系統(tǒng)默認的AppClassLoader: "+ClassLoader.getSystemClassLoader());
System.out.println("AppClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent());
System.out.println("ExtClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent().getParent());
System.out.println("------------------");
MyClassLoader myClassLoader1 = new MyClassLoader("loader1");
myClassLoader1.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz1 = myClassLoader1.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz1:" + clazz1);
Object object1 = clazz1.newInstance();
System.out.println(object1);
System.out.println(myClassLoader1.getClass().getClassLoader());
System.out.println("------------------");
MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1,"loader12");
myClassLoader2.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz2 = myClassLoader2.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz2:" + clazz2);
Object object2 = clazz2.newInstance();
System.out.println(object2);
System.out.println("------------------");
MyClassLoader myClassLoader3 = new MyClassLoader("loader12");
myClassLoader3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz3:" + clazz3);
Object object3 = clazz3.newInstance();
System.out.println(object3);
}
}
代碼中,我們自定義了一個MyClassLoader,這里我們繼承了ClassLoader而非URLClassLoader,因此需要自己編寫findClass()方法邏輯以及加載字節(jié)碼的邏輯,接著在main方法中,通過ClassLoader.getSystemClassLoader()獲取到系統(tǒng)默認類加載器,通過獲取其父類加載器及其父父類加載器,同時還獲取了自定義類加載器的父類加載器,最終輸出結(jié)果正如我們所預(yù)料的,AppClassLoader的父類加載器為ExtClassLoader,而ExtClassLoader沒有父類加載器。如果我們實現(xiàn)自己的類加載器,它的父加載器都只會是AppClassLoader。這里我們不妨看看Lancher的構(gòu)造器源碼:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 首先創(chuàng)建擴展類加載器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//再創(chuàng)建AppClassLoader并把擴展類加載器作為父加載器傳遞給AppClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//設(shè)置線程上下文類加載器,稍后分析
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
顯然Lancher初始化時首先會創(chuàng)建ExtClassLoader類加載器,然后再創(chuàng)建AppClassLoader并把ExtClassLoader傳遞給它作為父類加載器,這里還把AppClassLoader默認設(shè)置為線程上下文類加載器,關(guān)于線程上下文類加載器稍后會分析。那ExtClassLoader類加載器為什么是null呢?看下面的源碼創(chuàng)建過程就明白,在創(chuàng)建ExtClassLoader強制設(shè)置了其父加載器為null。
// 首先創(chuàng)建擴展類加載器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
public ExtClassLoader(File[] var1) throws IOException {
//調(diào)用父類構(gòu)造URLClassLoader傳遞null作為parent
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
}
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
acc = AccessController.getContext();
ucp = new URLClassPath(urls, factory, acc);
}
顯然ExtClassLoader的父類為null,而AppClassLoader的父加載器為ExtClassLoader,所有自定義的類加載器其父加載器只會是AppClassLoader,注意這里所指的父類并不是Java繼承關(guān)系中的那種父子關(guān)系。
類與類加載器
在JVM中表示兩個class對象是否為同一個類對象存在兩個必要條件
- 類的全限定名必須一致,包括包名。
- 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同。
也就是說,在JVM中,即使這個兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象也是不相等的,這是因為不同的ClassLoader實例對象都擁有不同的獨立的類名稱空間,所以加載的class對象也會存在不同的類名空間中,但前提是覆寫loadclass方法,從前面雙親委派模式對loadClass()方法的源碼分析中可以知,在方法第一步會通過Class<?> c = findLoadedClass(name);從緩存查找,類名完整名稱相同則不會再次被加載,因此我們必須繞過緩存查詢才能重新加載class對象。當然也可直接調(diào)用findClass()方法,這樣也避免從緩存查找。如果不從緩存查詢相同完全類名的class對象,那么只有ClassLoader的實例對象不同,同一字節(jié)碼文件創(chuàng)建的class對象自然也不會相同。
了解class文件的顯示加載與隱式加載的概念
所謂class文件的顯示加載與隱式加載的方式是指JVM加載class文件到內(nèi)存的方式,顯示加載指的是在代碼中通過調(diào)用ClassLoader加載class對象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加載class對象。
隱式加載則是不直接在代碼中調(diào)用ClassLoader的方法加載class對象,而是通過虛擬機自動加載到內(nèi)存中,如在加載某個類的class文件時,該類的class文件中引用了另外一個類的對象,此時額外引用的類將通過JVM自動加載到內(nèi)存中。在日常開發(fā)以上兩種方式一般會混合使用,這里我們知道有這么回事即可。
編寫自己的類加載器
通過前面的分析可知,實現(xiàn)自定義類加載器需要繼承ClassLoader或者URLClassLoader,繼承ClassLoader則需要自己重寫findClass()方法并編寫加載邏輯,繼承URLClassLoader則可以省去編寫findClass()方法以及class文件加載轉(zhuǎn)換成字節(jié)碼流的代碼。那么編寫自定義類加載器的意義何在呢?
- 當class文件不在ClassPath路徑下,默認系統(tǒng)類加載器無法找到該class文件,在這種情況下我們需要實現(xiàn)一個自定義的ClassLoader來加載特定路徑下的class文件生成class對象。
- 當一個class文件是通過網(wǎng)絡(luò)傳輸并且可能會進行相應(yīng)的加密操作時,需要先對class文件進行相應(yīng)的解密后再加載到JVM內(nèi)存中,這種情況下也需要編寫自定義的ClassLoader并實現(xiàn)相應(yīng)的邏輯。
- 當需要實現(xiàn)熱部署功能時(一個class文件通過不同的類加載器產(chǎn)生不同class對象從而實現(xiàn)熱部署功能),需要實現(xiàn)自定義ClassLoader的邏輯。
繼承ClassLoader實現(xiàn)自定義類加載器
public class MyClassLoader extends ClassLoader {
private String classLoaderName;
private String path;
private final String fileExtension = ".class";
public MyClassLoader(String classLoaderName){
super();//將系統(tǒng)類加載器作為該類加載器的父加載器
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader parent,String classLoaderName){
super(parent);//顯示指定該類加載器的父加載器
this.classLoaderName = classLoaderName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "MyTest15{" +
"classLoaderName='" + classLoaderName + '\'' +
'}';
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("findClass invoked:"+className);
System.out.println("class loader name:"+this.classLoaderName);
byte[] data = this.loadClassData(className);
if(data == null){
throw new ClassNotFoundException();
}
return this.defineClass(className,data,0,data.length);
}
private byte[] loadClassData(String className){
InputStream inputStream = null;
byte[] data = null;
ByteArrayOutputStream bos = null;
try {
this.path = path.replace(".","/");
this.classLoaderName = classLoaderName.replace(".","/");
inputStream = new FileInputStream(new File(this.path + className + this.classLoaderName));
bos = new ByteArrayOutputStream();
int ch;
while((ch = inputStream.read()) != -1){
bos.write(ch);
}
data = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader1 = new MyClassLoader("loader1");
myClassLoader1.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz1 = myClassLoader1.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz1:" + clazz1);
Object object1 = clazz1.newInstance();
System.out.println(object1);
System.out.println(myClassLoader1.getClass().getClassLoader());
System.out.println("------------------");
MyClassLoader myClassLoader2 = new MyClassLoader(myClassLoader1,"loader12");
myClassLoader2.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz2 = myClassLoader2.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz2:" + clazz2);
Object object2 = clazz2.newInstance();
System.out.println(object2);
System.out.println("------------------");
MyClassLoader myClassLoader3 = new MyClassLoader("loader12");
myClassLoader3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/");
Class<?> clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println("clazz3:" + clazz3);
Object object3 = clazz3.newInstance();
System.out.println(object3);
}
}
顯然我們通過loadClassData()方法找到class文件并轉(zhuǎn)換為字節(jié)流,并重寫findClass()方法,利用defineClass()方法創(chuàng)建了類的class對象。在main方法中調(diào)用了setPath()方法加載指定路徑下的class文件,由于啟動類加載器、拓展類加載器以及系統(tǒng)類加載器都無法在其路徑下找到該類,因此最終將有自定義類加載器加載,即調(diào)用findClass()方法進行加載。如果繼承URLClassLoader實現(xiàn),那代碼就更簡潔了,如下:
繼承URLClassLoader現(xiàn)自定義類加載器
public class MyURLClassLoader extends URLClassLoader {
public MyURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public MyURLClassLoader(URL[] urls) {
super(urls);
}
public MyURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
public static void main(String[] args) throws MalformedURLException {
String rootDir="E:/gradleproject/jvm_study/build/classes/java/main/";
//創(chuàng)建自定義文件類加載器
File file = new File(rootDir);
//File to URI
URI uri=file.toURI();
URL[] urls = {uri.toURL()};
MyURLClassLoader loader = new MyURLClassLoader(urls);
try {
//加載指定的class文件
Class<?> object =loader.loadClass("com.yibo.jvm.classloader.MyTest1");
System.out.println(object.newInstance().toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
非常簡潔除了需要重寫構(gòu)造器外無需編寫findClass()方法及其class文件的字節(jié)流轉(zhuǎn)換邏輯。
自定義網(wǎng)絡(luò)類加載器
自定義網(wǎng)絡(luò)類加載器,主要用于讀取通過網(wǎng)絡(luò)傳遞的class文件(在這里我們省略class文件的解密過程),并將其轉(zhuǎn)換成字節(jié)流生成對應(yīng)的class對象,如下
public class NetClassLoader extends ClassLoader {
private String url;//class文件的URL
public NetClassLoader(String url) {
this.url = url;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] data = this.loadClassData(className);
if(data == null){
throw new ClassNotFoundException();
}
return this.defineClass(className,data,0,data.length);
}
private byte[] loadClassData(String className){
String path = classNameToPath(className);
InputStream ins = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
URL url = new URL(path);
ins = url.openStream();
baos = new ByteArrayOutputStream();
int bufferSize = 8192;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
// 讀取類文件的字節(jié)
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
//這里省略解密的過程.......
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(ins != null){
try {
ins.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(baos != null){
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return data;
}
private String classNameToPath(String className) {
// 得到類文件的URL
return url + "/" + className.replace('.', '/') + ".class";
}
}
主要是在獲取字節(jié)碼流時的區(qū)別,從網(wǎng)絡(luò)直接獲取到字節(jié)流再轉(zhuǎn)車字節(jié)數(shù)組然后利用defineClass方法創(chuàng)建class對象,如果繼承URLClassLoader類則和前面文件路徑的實現(xiàn)是類似的,無需擔心路徑是filePath還是Url,因為URLClassLoader內(nèi)的URLClassPath對象會根據(jù)傳遞過來的URL數(shù)組中的路徑判斷是文件還是jar包,然后根據(jù)不同的路徑創(chuàng)建FileLoader或者JarLoader或默認類Loader去讀取對于的路徑或者url下的class文件。
熱部署類加載器
所謂的熱部署就是利用同一個class文件不同的類加載器在內(nèi)存創(chuàng)建出兩個不同的class對象(關(guān)于這點的原因前面已分析過,即利用不同的類加載實例),由于JVM在加載類之前會檢測請求的類是否已加載過(即在loadClass()方法中調(diào)用findLoadedClass()方法),如果被加載過,則直接從緩存獲取,不會重新加載。注意同一個類加載器的實例和同一個class文件只能被加載器一次,多次加載將報錯,因此我們實現(xiàn)的熱部署必須讓同一個class文件可以根據(jù)不同的類加載器重復加載,以實現(xiàn)所謂的熱部署。實際上前面的實現(xiàn)的MyClassLoader和MyURLClassLoader已具備這個功能,但前提是直接調(diào)用findClass()方法,而不是調(diào)用loadClass()方法,因為ClassLoader中l(wèi)oadClass()方法體中調(diào)用findLoadedClass()方法進行了檢測是否已被加載,因此我們直接調(diào)用findClass()方法就可以繞過這個問題,當然也可以重新loadClass方法,但強烈不建議這么干。
雙親委派模型的破壞者-線程上下文類加載器
當前類加載器(current ClassLoader)
- 每個類都會使用自己的類加載器(即加載自身的類加載器)來加載其他類(值的是所依賴的類)
- 如果ClassX引用了ClassY,那么ClassX的類加載器就會去加載ClassY(前提是ClassY尚未被加載)
線程上下文類加載器(Context ClassLoader)
- 線程上下文類加載器(Context ClassLoader)是從JDK1.2開始引入的,類Thread中的getContextClassLoader()與setContextClassLoader(ClassLoader cl)分別用來獲取和設(shè)置線程上下文類加載器
- 如果沒有通過setContextClassLoader(ClassLoader cl)進行設(shè)置的話,線程將繼承其父線程的上下文類加載器
- Java應(yīng)用運行時的初始線程的上下文類加載器是系統(tǒng)類加載器,在線程中運行的代碼可以通過該類加載器來加載類與資源
線程上下文類加載器的重要性:
- 在Java應(yīng)用中存在著很多服務(wù)提供者接口(Service Provider Interface,SPI),這些接口允許第三方為它們提供實現(xiàn),如常見的 SPI 有 JDBC、JNDI等,這些 SPI 的接口屬于 Java 核心庫,一般存在rt.jar包中,由Bootstrap類加載器加載,而 SPI 的第三方實現(xiàn)代碼則是作為Java應(yīng)用所依賴的 jar 包被存放在classpath路徑下,由于SPI接口中的代碼經(jīng)常需要加載具體的第三方實現(xiàn)類并調(diào)用其相關(guān)方法,但SPI的核心接口類是由引導類加載器來加載的,而Bootstrap類加載器無法直接加載SPI的實現(xiàn)類
- 父ClassLoader可以使用當前線程Thread.currentThread().getContextClassLoader所指定的ClassLoader加載的類,這就改變了父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關(guān)系的ClassLoader加載的類的情況,即改變了雙親委托模型
- 線程上下文類加載器就是當前線程的Current ClassLoader,在雙親委托模型下,類加載是由下至上的,即下層的類加載器會委托上層進行加載。但是對于SPI來說,有些接口是Java核心庫所提供的,而Java核心庫是由啟動類加載器所加載的,而這些接口的實現(xiàn)確來自于不同的jar包(廠商提供),Java的啟動類加載器是不會加載其他來源的jar包,這樣傳統(tǒng)的雙親委托模型就無法滿足SPI的要求,而通過給當前線程設(shè)置上下文類加載器,就可以由設(shè)置的上下文類加載器來實現(xiàn)對于接口的實現(xiàn)類的加載。
如下圖所示,以jdbc.jar加載為例:

從圖可知rt.jar核心包是由Bootstrap類加載器加載的,其內(nèi)包含SPI核心接口類,由于SPI中的類經(jīng)常需要調(diào)用外部實現(xiàn)類的方法,而jdbc.jar包含外部實現(xiàn)類(jdbc.jar存在于classpath路徑)無法通過Bootstrap類加載器加載,因此只能委派線程上下文類加載器把jdbc.jar中的實現(xiàn)類加載到內(nèi)存以便SPI相關(guān)類使用。顯然這種線程上下文類加載器的加載方式破壞了“雙親委派模型”,它在執(zhí)行過程中拋棄雙親委派加載鏈模式,使程序可以逆向使用類加載器,當然這也使得Java類加載器變得更加靈活。為了進一步證實這種場景,不妨看看DriverManager類的源碼,DriverManager是Java核心rt.jar包中的類,該類用來管理不同數(shù)據(jù)庫的實現(xiàn)驅(qū)動即Driver,它們都實現(xiàn)了Java核心包中的java.sql.Driver接口,如mysql驅(qū)動包中的com.mysql.jdbc.Driver,這里主要看看如何加載外部實現(xiàn)類,在DriverManager初始化時會執(zhí)行如下代碼
//DriverManager是Java核心包rt.jar的類
public class DriverManager {
//省略不必要的代碼......
static {
loadInitialDrivers();//執(zhí)行該方法
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
//省略不必要的代碼......
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//加載外部的Driver的實現(xiàn)類
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
//省略不必要的代碼......
}
}
在DriverManager類初始化時執(zhí)行了loadInitialDrivers()方法,在該方法中通過ServiceLoader.load(Driver.class);去加載外部實現(xiàn)的驅(qū)動類,ServiceLoader類會去讀取mysql的jdbc.jar下META-INF文件的內(nèi)容,如下所示:

而com.mysql.jdbc.Driver繼承類如下:
/**
* Backwards compatibility to support apps that call <code>Class.forName("com.mysql.jdbc.Driver");</code>.
*/
public class Driver extends com.mysql.cj.jdbc.Driver {
public Driver() throws SQLException {
super();
}
static {
System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
+ "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
}
}
從注釋可以看出平常我們使用com.mysql.jdbc.Driver已被丟棄了,取而代之的是com.mysql.cj.jdbc.Driver,也就是說官方不再建議我們使用如下代碼注冊mysql驅(qū)動
//不建議使用該方式注冊驅(qū)動類
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通過java庫獲取數(shù)據(jù)庫連接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");
而是直接去掉注冊步驟,如下即可:
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通過java庫獲取數(shù)據(jù)庫連接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");
這樣ServiceLoader會幫助我們處理一切,并最終通過load()方法加載,看看load()方法實現(xiàn)
public static <S> ServiceLoader<S> load(Class<S> service) {
//通過線程上下文類加載器加載
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
很明顯了確實通過線程上下文類加載器加載的,實際上核心包的SPI類對外部實現(xiàn)類的加載都是基于線程上下文類加載器執(zhí)行的,通過這種方式實現(xiàn)了Java核心代碼內(nèi)部去調(diào)用外部實現(xiàn)類。我們知道線程上下文類加載器默認情況下就是AppClassLoader,那為什么不直接通過getSystemClassLoader()獲取類加載器來加載classpath路徑下的類的呢?其實是可行的,但這種直接使用getSystemClassLoader()方法獲取AppClassLoader加載類有一個缺點,那就是代碼部署到不同服務(wù)時會出現(xiàn)問題,如把代碼部署到Java Web應(yīng)用服務(wù)或者EJB之類的服務(wù)將會出問題,因為這些服務(wù)使用的線程上下文類加載器并非AppClassLoader,而是Java Web應(yīng)用服自家的類加載器,類加載器不同。,所以我們應(yīng)用該少用getSystemClassLoader()??傊煌姆?wù)使用的可能默認ClassLoader是不同的,但使用線程上下文類加載器總能獲取到與當前程序執(zhí)行相同的ClassLoader,從而避免不必要的問題。ok~.關(guān)于線程上下文類加載器暫且聊到這,前面闡述的DriverManager類,大家可以自行看看源碼,相信會有更多的體會,另外關(guān)于ServiceLoader本篇并沒有過多的闡述,畢竟我們主題是類加載器,但ServiceLoader是個很不錯的解耦機制,大家可以自行查閱其相關(guān)用法。
線程上下文類加載器的一般使用模式(獲取 - 使用 - 還原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
Thread.currentThread().setContextClassLoader(targetTClassLoader);
myMethod();
}finally{
Thread.currentThread().setContextClassLoader(classLoader);
}
- myMethod()里面調(diào)用了Thread.currentThread().getContextClassLoader()獲取當前線程的上下文類加載器做某些事情
- 如果一個類由類加載器A加載,那么這個類的依賴類也是由相同的類加載器所加載的(前提是該類沒有被加載)
- ContextClassLoader的作用就是為了破壞Java類加載的雙親委托機制
- 當高層提供了統(tǒng)一的接口讓低層去實現(xiàn),同時又要在高層加載(或?qū)嵗?低層的類時,就必須要通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類
線程上下文類加載器的適用場景:
當高層提供了統(tǒng)一接口讓低層去實現(xiàn),同時又要是在高層加載(或?qū)嵗┑蛯拥念悤r,必須通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類。
當使用本類托管類加載,然而加載本類的ClassLoader未知時,為了隔離不同的調(diào)用者,可以取調(diào)用者各自的線程上下文類加載器代為托管。