作者 某人Valar
如需轉載請保留原文連結本文涉及到的Java原始碼均為
Java8
版本部分圖片來自百度,如有侵權請聯絡刪除
目錄:
- 類載入器
- java.lang.ClassLoader類
- URLClassLoader與SecureClassLoader
- ClassLoader常見方法原始碼分析
- 雙親委託機制
- 圖解
- 原始碼角度分析
- 常見的問題分析
前言:我們剛剛接觸Java時,在IDE(整合開發環境) 或者文字編輯器中所寫的都是.java檔案,在編譯後會生成.class檔案,又稱位元組碼檔案。
javac HelloWorld.java ---> HelloWorld.class 複製程式碼
對於.class檔案來說,需要被載入到虛擬機器中才能使用,這個載入的過程就成為類載入。如果想要知道類載入的方式,就需要知道類載入器和雙親委託機制的概念。也就是我們本篇所要介紹的內容。
1. 類載入器
Java中的類載入器可以分為兩種:
- 系統類載入器
- 自定義類載入器
而系統類載入器又有3個:
- Bootstrap ClassLoader:啟動類載入器
- Extensions ClassLoader:擴充套件類載入器
- App ClassLoader:也稱為SystemAppClass,系統類載入器
1.1 Bootstrap ClassLoader
Bootstrap ClassLoader用來載入JVM(Java虛擬機器)
執行時所需要的系統類,其使用c++
實現。
從以下路徑來載入類:
%JAVA_HOME%/jre/lib
目錄,如rt.jar、resources.jar、charsets.jar等- 可以在JVM啟動時,指定-Xbootclasspath引數,來改變Bootstrap ClassLoader的載入目錄。
Java虛擬機器的啟動就是通過 Bootstrap ClassLoader建立一個初始類來完成的。 可以通過如下程式碼來得出Bootstrap ClassLoader所載入的目錄:
public class ClassLoaderTest {
public static void main(String[]args) {
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
複製程式碼
列印結果為:
C:\Program Files\Java\jdk1.8.0_102\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_102\jre\classes
複製程式碼
可以發現幾乎都是$JAVA_HOME/jre/lib
目錄中的jar包,包括rt.jar、resources.jar和charsets.jar等等。
1.2 Extensions ClassLoader
Extensions ClassLoader(擴充套件類載入器)具體是由ExtClassLoader
類實現的,ExtClassLoader
類位於sun.misc.Launcher
類中,是其的一個靜態內部類。對於Launcher
類,可以先看成是Java虛擬機器的一個入口。
ExtClassLoader
的部分程式碼如下:
Extensions ClassLoader負責將JAVA_HOME/jre/lib/ext
或者由系統變數-Djava.ext.dir
指定位置中的類庫載入到記憶體中。
通過以下程式碼可以得到Extensions ClassLoader載入目錄:
System.out.println(System.getProperty("java.ext.dirs"));
複製程式碼
列印結果為:
C:\Program Files\Java\jdk1.8.0_102\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext
複製程式碼
1.3 App ClassLoader
也稱為SystemAppClass(系統類載入器),具體是由AppClassLoader
類實現的,AppClassLoader
類也位於sun.misc.Launcher
類中。
部分程式碼如下:
- 主要載入Classpath目錄下的的所有jar和Class檔案,是程式中的預設類載入器。這裡的Classpath是指我們Java工程的bin目錄。
- 也可以載入通過-Djava.class.path選項所指定的目錄下的jar和Class檔案。
通過以下程式碼可以得到App ClassLoader載入目錄:
System.out.println(System.getProperty("java.class.path"));
複製程式碼
列印結果為:
C:\workspace\Demo\bin
複製程式碼
這個路徑其實就是當前Java工程目錄bin,裡面存放的是編譯生成的class檔案。
在Java中,除了上述的3種系統提供的類載入器,還可以自定義一個類載入器。
1.4. 自定義類載入器
為了可以從指定的目錄下載入jar包或者class檔案,我們可以用繼承java.lang.ClassLoader類的方式來實現一個自己的類載入器。
在自定義類載入器時,我們一般複寫findClass
方法,並在findClass
方法中呼叫defineClass
方法。
接下來會先介紹下ClassLoader類相關的具體內容,之後看一個自定義類載入器demo。
2 java.lang.ClassLoader類
2.1 ClassLoader、URLClassLoader與SecureClassLoader的關係
從上面關於ExtClassLoader、AppClassLoader原始碼圖中我們可以看到,他們都繼承自URLClassLoader,那這個URLClassLoader是什麼,其背後又有什麼呢?
先來一張很重要的繼承關係圖:
- ClassLoader是一個抽象類,位於java.lang包下,其中定義了ClassLoader的主要功能。
- SecureClassLoader繼承了抽象類ClassLoader,但SecureClassLoader並不是ClassLoader的實現類,而是擴充了ClassLoader類加入了許可權方面的功能,加強了ClassLoader的安全性。
- URLClassLoader繼承自SecureClassLoader,用來通過URl路徑從jar檔案和資料夾中載入類和資源。
- ExtClassLoader和AppClassLoader都繼承自URLClassLoader,它們都是Launcher 的內部類,Launcher 是Java虛擬機器的入口應用,ExtClassLoader和AppClassLoader都是在Launcher中進行初始化的。
2.2 普通的類、AppClassLoader與ExtClassLoader之間的關係
關係:
- 載入普通的類(這裡指得是我們所編寫的程式碼類,下文demo中的Test類)載入器是AppClassLoader,AppClassLoader的父載入器為ExtClassLoader
- 而ExtClassLoader的父載入器是Bottstrap ClassLoader
還有2個結論:
- 每個類都有類載入器
- 每個類載入器都有父載入器
我們準備一個簡單的demo 自建的一個Test.java檔案。
public class Test{}
複製程式碼
public class Main {
public static void main(String[] args) {
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
}
}
複製程式碼
這樣就可以獲取到Test.class檔案的類載入器,然後列印出來。結果是:
sun.misc.Launcher$AppClassLoader@75b83e92
複製程式碼
也就是說明Test.class檔案是由AppClassLoader載入的。
那AppClassLoader是誰載入的呢? 其實AppClassLoader也有一個父載入器,我們可以通過以下程式碼獲取
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader);
loader = loader.getParent();
}
}
}
複製程式碼
上述程式碼結果如下:
sun.misc.Launcher$AppClassLoader@7565783b
sun.misc.Launcher$ExtClassLoader@1b586d23
複製程式碼
- 載入Test的類載入器是AppClassLoader,AppClassLoader的父載入器為ExtClassLoader
- 而ExtClassLoader的父載入器是Bottstrap ClassLoader
至於為何沒有列印出ExtClassLoader的父載入器Bootstrap ClassLoader,這是因為Bootstrap ClassLoader是由C++編寫的,並不是一個Java類,因此我們無法在Java程式碼中獲取它的引用。
2.3 java.lang.ClassLoader類常見的方法
上一節我們看到了ClassLoader的getParent
方法,getParent
獲取到的其實就是其父載入器。這一節將通過原始碼,來介紹ClassLoader中的一些重要方法。
getParent()
ClassLoader類
---------
public final ClassLoader getParent() {
if (parent == null) return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
複製程式碼
我們可以看到,其返回值有兩種可能,為空
或者是parent
變數。
從原始碼中還可以發現其是一個final修飾的方法,我們知道被final修飾的說明這個方法提供的功能已經滿足當前要求,是不可以重寫的, 所以其各個子類所呼叫的
getParent()
方法最終都會由ClassLoader來處理。
parent變數又是什麼呢?我們在檢視原始碼時可以發現parent的賦值是在構造方法中。
ClassLoader類
---------
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
... //省略了無關程式碼
}
複製程式碼
而此構造方法又是私有的,不能被外部呼叫,所以其呼叫者還是在內部。於是接著查詢到了另外兩個構造方法。
ClassLoader類
---------
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
複製程式碼
所以:
- 可以在呼叫ClassLoder的構造方法時,指定一個parent。
- 若沒有指定的話,會使用
getSystemClassLoader()
方法的返回值。
接著看上面程式碼中的getSystemClassLoader的原始碼:
ClassLoader類
---------
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
複製程式碼
其返回的是一個scl。在initSystemClassLoader()
方法中發現了對scl變數的賦值。
ClassLoader類
---------
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); //1
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
...//省略程式碼
}
sclSet = true;
}
}
複製程式碼
重點來了,註釋1處其獲取到的是Launcher
類的物件,然後呼叫了Launcher
類的getClassLoader()
方法。
Launcher類
---------
public ClassLoader getClassLoader() {
return this.loader;
}
複製程式碼
那這個this.loader是什麼呢?在Launcher
類中發現,其賦值操作在Launcher
的構造方法中,其值正是Launcher
類中的AppClassLoader:
Launcher類
---------
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
...
}
複製程式碼
到這裡謎團全部解開了:
在建立ClassLoder時,
- 可以指定一個ClassLoder作為其parent,也就是其父載入器。
- 若沒有指定的話,會使用
getSystemClassLoader()
方法的返回值(也就是Launcher
類中的AppClassLoader)作為其parent。 - 通過getParent()方法可以獲取到這個父載入器。
defineClass()
能將class二進位制內容轉換成Class物件,如果不符合要求的會丟擲異常,例如ClassFormatError
、NoClassDefFoundError
。
在自定義ClassLoader時,我們通常會先將特定的檔案讀取成byte[]物件,再使用此方法,將其轉為class物件。
ClassLoader類
---------
/**
* String name:表示預期的二進位制檔名稱,不知道的話,可以填null。
* byte[] b:此class檔案的二進位制資料
* int off:class二進位制資料開始的位置
* int len:class二進位制資料的總長度
*/
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
複製程式碼
findClass()
findClass()
方法一般被loadClass()
方法呼叫去載入指定名稱類。
ClassLoader類
---------
/**
* String name:class檔案的名稱
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
複製程式碼
通過原始碼看到ClassLoader類中並沒有具體的邏輯,而是等待著其子類去實現,通過上面的分析我們知道兩個系統類載入器ExtClassLoader
和AppClassLoader
都繼承自URLClassLoader
,那就來看一下URLClassLoader
中的具體程式碼。
URLClassLoader類
---------
protected Class<?> findClass(final String name) throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
...
return result;
}
private Class<?> defineClass(String name, Resource res) throws IOException {
...
URL url = res.getCodeSourceURL();
...
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
...
return defineClass(name, bb, cs);
} else {
byte[] b = res.getBytes();
...
return defineClass(name, b, 0, b.length, cs);
}
}
複製程式碼
可以看到其對傳入的name進行處理後,就呼叫了defineClass(name, res)
;在這個方法裡主要是通過res資源和url,載入出相應格式的檔案,最終還是通過ClassLoader的defineClass
方法載入出具體的類。
loadClass()
上節說到findClass()
一般是在loadClass()
中呼叫,那loadClass()
是什麼呢?
其實loadClass()
就是雙親委託機制的具體實現,所以在我們先介紹下雙親委託機制後,再來分析loadClass()
。
3 雙親委託機制介紹
3.1 圖解雙親委託機制
先簡單介紹下雙親委託機制: 類載入器查詢Class(也就是在loadClass時)所採用的是雙親委託模式,所謂雙親委託模式就是
- 首先判斷該Class是否已經載入
- 如果沒有則不是自身去查詢而是委託給父載入器進行查詢,這樣依次的進行遞迴,直到委託到最頂層的Bootstrap ClassLoader
- 如果Bootstrap ClassLoader找到了該Class,就會直接返回
- 如果沒找到,則繼續依次向下查詢,如果還沒找到則最後會交由自身去查詢
- 其中紅色的箭頭代表向上委託的方向,如果當前的類載入器沒有從快取中找到這個class物件,就會請求父載入器進行操作。直到
Bootstrap ClassLoader
。 - 而黑色的箭頭代表的是查詢方向,若
Bootstrap ClassLoader
可以從%JAVA_HOME%/jre/lib
目錄或者-Xbootclasspath指定目錄查詢到,就直接返回該物件,否則就讓ExtClassLoader
去查詢。 ExtClassLoader
就會從JAVA_HOME/jre/lib/ext
或者-Djava.ext.dir
指定位置中查詢,找不到時就交給AppClassLoader
,AppClassLoader
就從當前工程的bin目錄下查詢- 若還是找不到的話,就由我們自定義的
CustomClassLoader
查詢,具體查詢的結果,就要看我們怎麼實現自定義ClassLoader的findClass
方法了。
3.2 原始碼分析雙親委託機制
接下來我們看看雙親委託機制在原始碼中是如何體現的。 先看loadClass的原始碼:
ClassLoader類
---------
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,根據name檢查類是否已經載入,若已載入,會直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//若當前類載入器有父載入器,則呼叫其父載入器的loadClass()
c = parent.loadClass(name, false);
} else {
//若當前類載入器的parent為空,則呼叫findBootstrapClassOrNull()
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 1.如果到這裡c依然為空的話,表示一直到最頂層的父載入器也沒有找到已載入的c,那就會呼叫findClass進行查詢
// 2.在findClass的過程中,如果指定目錄下沒有,就會丟擲異常ClassNotFoundException
// 3.丟擲異常後,此層呼叫結束,接著其子載入器繼續進行findClass操作
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
複製程式碼
findBootstrapClassOrNull()方法:可以看到其對name進行校驗後,最終呼叫了一個native
方法findBootstrapClass()
。在findBootstrapClass()
方法中最終會用Bootstrap Classloader來查詢類。
ClassLoader類
---------
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name)) return null;
return findBootstrapClass(name);
}
private native Class<?> findBootstrapClass(String name);
複製程式碼
4 常見的問題
4.1 為什麼使用雙親委託機制?
- 避免重複載入,如果已經載入過一次Class,就不需要再次載入,而是先從快取中直接讀取。
- 安全方面的考慮,如果不使用雙親委託模式,就可以自定義一個String類來替代系統的String類,這樣便會造成安全隱患,採用雙親委託模式會使得系統的String類在Java虛擬機器啟動時就被載入,也就無法自定義String類來替代系統的String類。
4.2 由不同的類載入器載入的類會被JVM當成同一個類嗎?
不會。 在Java中,我們用包名+類名作為一個類的標識。 但在JVM中,一個類用其包名+類名和一個ClassLoader的例項作為唯一標識,不同類載入器載入的類將被置於不同的名稱空間.
通過一個demo來看,
- 用兩個自定義類載入器去載入一個自定義的類
- 然後獲取到的Class進行java.lang.Object.equals(…)判斷。
public class Main {
public static void main(String[] args) {
ClassLoaderTest myClassLoader = new ClassLoaderTest("F:\\");
ClassLoaderTest myClassLoader2 = new ClassLoaderTest("F:\\");
try {
Class c = myClassLoader.loadClass("com.example.Hello");
Class c2 = myClassLoader.loadClass("com.example.Hello");
Class c3 = myClassLoader2.loadClass("com.example.Hello");
System.out.println(c.equals(c2)); //true
System.out.println(c.equals(c3)); //flase
}
}
複製程式碼
輸出結果:
true
false
複製程式碼
只有兩個類名一致並且被同一個類載入器載入的類,Java虛擬機器才會認為它們是同一個類。
上面demo中用到的自定義ClassLoader:
自定義的類載入器
注意點:
1.覆寫findClass方法
2.讓其可以根據name從我們指定的path中載入檔案,也就是將檔案正確轉為byte[]格式
3.使用defineClass方法將byte[]資料轉為Class物件
-------------
public class ClassLoaderTest extends ClassLoader{
private String path;
public ClassLoaderTest(String path) {
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
byte[] classData = classToBytes(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
clazz= defineClass(name, classData, 0, classData.length);
}
return clazz;
}
private byte[] classToBytes(String name) {
String fileName = getFileName(name);
File file = new File(path,fileName);
InputStream in=null;
ByteArrayOutputStream out=null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length=0;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
return out.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(in!=null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try{
if(out!=null) {
out.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
return null;
}
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
複製程式碼
結語
到此Java的類載入器以及雙親委託機制都講了個大概,如果文中有錯誤的地方、或者有其他關於類載入器比較重要的內容又沒有介紹到的,歡迎在評論區裡留言,一起交流學習。
下一篇會說道Java new一個物件的過程,其中會涉及到類的載入、驗證,以及物件建立過程中的堆記憶體分配等內容。