我們知道,Java程序的啟動,其實是依賴于我們編譯之后的.class文件,那么JVM是如何讀取.class文件的?這一切都離不開ClassLoader這個類,ClassLoader可以將Java字節碼加載入JVM當中,供后面使用。
前言
一個Java程序的啟動流程,大致分為:編譯->加載->鏈接->初始化->運行。
- 編譯,也就是我們常見的將
.java
文件轉換為.class
文件的過程。一般我們可以通過javac
命令進行編譯,或者在ide中我們build
一下就進行了編譯 - 加載,也就是這個環節利用了ClassLoader,將
.class
文件加載進JVM中 - 鏈接,包括驗證、準備和解析等幾個步驟
- 初始化,主要是執行了類中的靜態代碼
- 運行,顧名思義,通過JVM虛擬機在對應平臺上運行程序代碼
三個類加載器
我們知道Java程序的啟動,需要類加載器ClassLoader
來將java字節碼載入JVM,那么ClassLoader
又是從何處開始加載呢?
概述
在Java中,系統自帶了三個類記載器:
-
BootStrap ClassLoader
最頂層的類加載器,Java會最先啟動這個類加載器,加載核心類庫,加載%JRE_HOME%\lib
目錄下的jar包和class文件等,其中就包括了Java的基礎類,如String,IO等等 -
Extention ClassLoader
第二層的類加載器,加載拓展類庫,會加載%JRE_HOME%\lib\ext
目錄下的jar包和class文件等,其中包括了各地的時間語言包等。 -
App ClassLoader
第三層的類加載庫,加載當前應用目錄下的jar包和class文件。
加載順序
-
BootStrap ClassLoader
剛才說了,BootStrap ClassLoader
是最頂層的類加載器,負責加載核心類庫,自然也就是最先加載的ClassLoader了,然而我們卻沒辦法在Java中找到對應的類。黑人問號臉???
剛才也說了,String,File
等基礎類都是由他在最開始加載的。我們嘗試調用以下代碼:
System.out.println(String.class.getClassLoader());
結果卻是null
???
通過查閱資料可以得知,BootStrap ClassLoaer
其實是C/C++實現的,是它本身是虛擬機的一部分,所以我們在Java代碼中并沒有辦法找到他。所以自然也就是null
咯
-
Extention ClassLoader
&AppClassLoader
和BootStrap ClassLoader
不同,Extention ClassLoader
&AppClassLoader
我們可以在代碼中找到,他們都是Launcher
類的內部類:
public class Launcher {
public Launcher() {
//省略大量代碼
Launcher.ExtClassLoader var1;
//加載ExtClassLoader,沒有傳入參數
var1 = Launcher.ExtClassLoader.getExtClassLoader();
//加載AppClassLoader,傳入ExtClassLoader作為參數,并將AppClassLoader設為Launcher的ClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
……
}
static class AppClassLoader extends URLClassLoader {
//省略
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
}
}
static class ExtClassLoader extends URLClassLoader {
//省略
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
}
}
}
查閱資料可以得知,Launcher
是由JVM的入口應用,由虛擬機啟動時通過JNI啟動該類。
我們可以看到Launcher
的構造函數中,依次加載了ExtClassLoader
和AppClassLoader
。
所以我們可以總結出:JVM會首先加載BootStrap ClassLoader,接著加載ExtClassLoader,然后才加載AppClassLoader。
工作原理——雙親委托
我們可以先看看ClassLoader的構造函數:
public abstract class ClassLoader {
private final ClassLoader parent;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
……//省略一堆
}
}
可以看到,在構造函數中,傳入了一個ClassLoader作為parent。
同時ClassLoader有一個getParent的方法,可以通過這個方法獲取當前ClassLoader的Parent,那么AppClassLoader和ExtClassLoader的parent是什么鬼呢,我們實驗一下:
System.out.println(Test.class.getClassLoader());
System.out.println(Test.class.getClassLoader().getParent());
System.out.println(Test.class.getClassLoader().getParent().getParent());
打印結果:
sun.misc.Launcher$AppClassLoader@28d93b30
sun.misc.Launcher$ExtClassLoader@14ae5a5
null
看到這樣的輸出結果,我們難免會有些疑惑:
-
AppClassLoader
的parent是ExtClassLoader
? -
ExtClassLoader
的parent是null
? - 那
BootStrapClassLoader
呢???
為了解開這些疑惑,我們看一下AppClassLoader
&ExtClassLoader
的構造函數:
public class Launcher {
public Launcher() {
//省略大量代碼
Launcher.ExtClassLoader var1;
//加載ExtClassLoader,沒有傳入參數
var1 = Launcher.ExtClassLoader.getExtClassLoader();
//加載AppClassLoader,傳入ExtClassLoader作為參數,并將AppClassLoader設為Launcher的ClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
……
}
static class AppClassLoader extends URLClassLoader {
//省略
AppClassLoader(URL[] var1, ClassLoader var2) {
// var2對應傳入了ExtClassLoader作為parent
super(var1, var2, Launcher.factory);
}
}
static class ExtClassLoader extends URLClassLoader {
//省略
public ExtClassLoader(File[] var1) throws IOException {
// 傳入null作為parent
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
}
}
}
可以看到,Launcher
在構造時會先創建ExtClassLoader
然后將創建的ExtClassLoader
做為參數構建AppClassLoader
。而ExtClassLoader
創建時會傳入null
作為parent,AppClassLoader
會傳入剛構建的ExtClassLoader
作為parent。
這也就印證了我們剛才的輸出結果。但是parent的意義有是什么呢?
加載過程
接下來我們需要先看一下ClassLoader是如何加載一個Class的:
在Java會通過ClassLoader的loadClass
方法來加載一個類,我們先看一下這個方法:
public abstract class ClassLoader {
/**
* 使用特定的二進制名來加載類
* 默認會執行如下的順序:
* 1. 通過findLoadedClass()判斷這個類時候已經加載過了
* 2. 通過parent.loadClass()嘗試讓parent加載這個class
* 如果parent是null,那么調用虛擬機創建的ClassLoaer
* 3. 通過findClass()加載類,最終defineClass方法將jar文件轉為Class
*
* 如果通過上述步驟可以加載出對應的class,那么調用resolveClass(Class 處理結果。
* 子類應該重寫findClass()方法而不是這個方法
*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
//省略部分代碼
synchronized (getClassLoadingLock(name)) {
// 首先檢查這個類是否已經被加載過
Class<?> c = findLoadedClass(name);
//如果沒有被加載過
if (c == null) {
// 如果parent不為空
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果parent為空,那么調用Bootstrap classLoader加載這個類
c = findBootstrapClassOrNull(name);
}
if (c == null) {
// 如果仍然為空,那么調用findClass
c = findClass(name);
}
}
if (resolve) {
// 處理結果
resolveClass(c);
}
return c;
}
}
}
通過注釋我們可以清楚的看到這個方法的主要邏輯:
- 通過
findLoadedClass()
判斷這個類時候已經加載過了 - 通過
parent.loadClass()
嘗試讓parent加載這個class
如果parent是null
,那么調用虛擬機創建的BootStrapClassLoaer
- 通過
findClass()
(最終轉到defineClass方法)加載類,將jar文件轉為Class
TODO:= =本來打算畫個圖。??墒窃趺串嫸疾缓弦狻?。
可以看到,ClassLoader
會先委托parent
進行加載,如果加載不出來才會自己進行加載,所以Classloader
的parent
其實是一個層級關系。
這也正是剛才說的雙親委托機制:先委托parent進行加載,加載不出來再自己加載。
為什么要選擇這種機制呢?試想一下如果現在我也實現了一個String類(包名也一樣),這樣JVM中是不是就會有兩種String呢?這樣不就亂套了嗎,而且也出現了安全問題
采用雙親委托機制就很好的解決了這種情況,我們永遠沒有辦法加載一個自定義的String類(包名也相同)。
總結
首先JVM會依次加載
BootStrapClassLoader(C/C++)
,ExtClassLoader
,AppClassLoader
。AppClassLoader
的parent
是ExtClassLoader
,而ExtClassLoader
的parent
可以認為是BootStrapClassLoader
(因為當parent為null時,會調用BootStrapClassLoader來加載)ClasLoader
加載時采用雙親委托機制,會優先委托parent加載,如果null
,再自己加載