Java類加載機制詳解
一、類加載器
類加載器(ClassLoader),顧名思義,即加載類的東西。在我們使用一個類之前,JVM需要先將該類的字節碼文件(.class文件)從磁盤、網絡或其他來源加載到內存中,並對字節碼進行解析生成對應的Class對象,這就是類加載器的功能。我們可以利用類加載器,實現類的動態加載。
二、類的加載機制
在Java中,采用雙親委派機制來實現類的加載。那什麽是雙親委派機制?在Java Doc中有這樣一段描述:
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine’s built-in class loader, called the “bootstrap class loader”, does not itself have a parent but may serve as the parent of a ClassLoader instance.
從以上描述中,我們可以總結出如下四點:
1、類的加載過程采用委托模式實現
2、每個 ClassLoader 都有一個父加載器。
3、類加載器在加載類之前會先遞歸的去嘗試使用父加載器加載。
4、虛擬機有一個內建的啟動類加載器(Bootstrap ClassLoader),該加載器沒有父加載器,但是可以作為其他加載器的父加載器。
Java 提供三種類型的系統類加載器。第一種是啟動類加載器,由C++語言實現,屬於JVM的一部分,其作用是加載 /lib 目錄中的文件,並且該類加載器只加載特定名稱的文件(如 rt.jar),而不是該目錄下所有的文件。另外兩種是 Java 語言自身實現的類加載器,包括擴展類加載器(ExtClassLoader)和應用類加載器(AppClassLoader),擴展類加載器負責加載\lib\ext目錄中或系統變量 java.ext.dirs 所指定的目錄中的文件。應用程序類加載器負責加載用戶類路徑中的文件。用戶可以直接使用擴展類加載器或系統類加載器來加載自己的類,但是用戶無法直接使用啟動類加載器,除了這兩種類加載器以外,用戶也可以自定義類加載器,加載流程如下圖所示:
註意:這裏父類加載器並不是通過繼承關系來實現的,而是采用組合實現的。
我們可以通過一段程序來驗證這個過程:
/**
* Java學習交流QQ群:589809992 我們一起學Java!
*/
public class Test {
}
public class TestMain {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader!=null){
System.out.println(loader);
loader = loader.getParent();
}
}
}
上面程序的運行結果如下所示:
從結果我們可以看出,默認情況下,用戶自定義的類使用 AppClassLoader 加載,AppClassLoader 的父加載器為 ExtClassLoader,但是 ExtClassLoader 的父加載器卻顯示為空,這是什麽原因呢?究其緣由,啟動類加載器屬於 JVM 的一部分,它不是由 Java 語言實現的,在 Java 中無法直接引用,所以才返回空。但如果是這樣,該怎麽實現 ExtClassLoader 與 啟動類加載器之間雙親委派機制?我們可以參考一下源碼:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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();
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;
}
}
從源碼可以看出,ExtClassLoader 和 AppClassLoader都繼承自 ClassLoader 類,ClassLoader 類中通過 loadClass 方法來實現雙親委派機制。整個類的加載過程可分為如下三步:
1、查找對應的類是否已經加載。
2、若未加載,則判斷當前類加載器的父加載器是否為空,不為空則委托給父類去加載,否則調用啟動類加載器加載(findBootstrapClassOrNull 再往下會調用一個 native 方法)。
3、若第二步加載失敗,則調用當前類加載器加載。
通過上面這段程序,可以很清楚的看出擴展類加載器與啟動類加載器之間是如何實現委托模式的。
現在,我們再驗證另一個問題。我們將剛才的Test類打成jar包,將其放置在 \lib\ext 目錄下,然後再次運行上面的代碼,結果如下:
現在,該類就不再通過 AppClassLoader 來加載,而是通過 ExtClassLoader 來加載了。如果我們試圖把jar包拷貝到\lib,嘗試通過啟動類加載器加載該類時,我們會發現編譯器無法識別該類,因為啟動類加載器除了指定目錄外,還必須是特定名稱的文件才能加載。
三、自定義類加載器
通常情況下,我們都是直接使用系統類加載器。但是,有的時候,我們也需要自定義類加載器。比如應用是通過網絡來傳輸 Java 類的字節碼,為保證安全性,這些字節碼經過了加密處理,這時系統類加載器就無法對其進行加載,這樣則需要自定義類加載器來實現。自定義類加載器一般都是繼承自 ClassLoader 類,從上面對 loadClass 方法來分析來看,我們只需要運動康復中心重寫 findClass 方法即可。下面我們通過一個示例來演示自定義類加載器的流程:
package com.paddx.test.classloading;
import java.io.*;
/**
* Created by liuxp on 16/3/12.
*/
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace(‘.‘, File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("/Users/liuxp/tmp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.paddx.test.classloading.Test");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
運行上面的程序,輸出結果如下:
自定義類加載器的核心在於對字節碼文件的獲取,如果是加密的字節碼則需要在該類中對文件進行解密。由於這裏只是演示,我並未對class文件進行加密,因此沒有解密的過程。這裏有幾點需要註意:
1、這裏傳遞的文件名需要是類的全限定性名稱,即com.paddx.test.classloading.Test格式的,因為 defineClass 方法是按這種格式進行處理的。
2、最好不要重寫loadClass方法,因為這樣容易破壞雙親委托模式。
3、這類 Test 類本身可以被 AppClassLoader 類加載,因此我們不能把 com/paddx/test/classloading/Test.class 放在類路徑下。否則,由於雙親委托機制的存在,會直接導致該類由 AppClassLoader 加載,而不會通過我們自定義類加載器來加載。
四、總結
雙親委派機制能很好地解決類加載的統一性問題。對一個 Class 對象來說,如果類加載器不同,即便是同一個字節碼文件,生成的 Class 對象也是不等的。也就是說,類加載器相當於 Class 對象的一個命名空間。雙親委派機制則保證了基類都由相同的類加載器加載,這樣就避免了同一個字節碼文件被多次加載生成不同的 Class 對象的問題。但雙親委派機制僅僅是Java 規範所推薦的一種實現方式,它並不是強制性的要求。近年來,很多熱部署的技術都已不遵循這一規則,如 OSGi 技術就采用了一種網狀的結構,而非雙親委派機制。
Java類加載機制詳解