目錄
- 前言
- 一、雙親委派機制
-
- 1、各個類加載器之間的關系
- 2、自定義類加載器
- 3、雙親委派邏輯的實作
- 二、雙親委派機制的優缺點
- 三、打破雙親委派機制
- 總結
前言
上一節(2)JVM 類加載之類加載器初始化記錄了JVM自帶的3中類加載器,也分析了類加載器初始化的流程。那麼問題來了:
- 這麼幾種類加載器之間的關系是什麼?
- 有這麼幾種類加載器,怎麼确定一個類由哪個類加載器加載?
- 為什麼要分這麼幾種類加載器呢?一個類加載器不也能加載嗎?
- 能夠打破雙親委派機制嗎?
本文涉及到的源碼都在JDK的jre\lib\rt.jar包中。
一、雙親委派機制
1、各個類加載器之間的關系
雙親委派機制用在一個類被加載之前,因為需要判斷由哪個類加載器去加載這個類。雙親委派機制在Java運作一個類的流程中的位置如圖:
從圖中可以看出,引導類加載器是拓展類加載器的父加載器,拓展類加載器是應用程式類加載器的父加載器,應用程式類加載器是自定義類加載器的父加載器。在加載一個類之前,若沒有自定義類加載器,則預設是從應用程式類加載器開始加載,逐級委托父級類加載器,最終委托到引導類加載器。若引導類加載器不能加載需要加載的類,則委派拓展類加載器進行加載;若拓展類加載器仍不能進行加載,則委派應用程式類加載器來完成加載。若自定義了類加載器,并使用自定義類加載器對某個類進行加載,則從自定義類加載器開始逐級委托,然後逐級委派。
為什麼這幾種類加載器是這樣的關系呢?因為源碼是這樣設計的。
在下面分析之前,先放上一個類圖:
這是在IDEA中點選ExtClassLoader然後按組合鍵Ctrl + alt + U看見的類圖,其實這個類圖沒有畫完,我在visio中畫了一下:
可以發現,拓展類加載器雖然是應用程式類加載器的父級加載器,但應用程式類加載器并不是繼承于拓展類加載器,隻不過在應用程式類加載器的類中有個parent屬性,這個屬性是從ClassLoader繼承過來的,裡面存的值就是拓展類加載器對象的引用,是以看起來他們像是父子的關系。其它的加載器情況與上面的類似,自定義類加載器的parent是應用程式類加載器,拓展類加載器的parent是null,因為其父類加載器引導類加載器是用C++實作的,在Java裡擷取不到。
我在(2)JVM 類加載之類加載器初始化中記錄過,拓展類加載器和應用程式類加載器是在sun.misc.Launcher.getLauncher()這裡建立的,那麼我們具體再跟進一下,先進入sun.misc.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);
}
...
}
在(2)JVM 類加載之類加載器初始化中分析得到:
- var1是建立的拓展類加載器;
- this.loader是建立的應用程式類加載器。
繼續跟進getExtClassLoader():
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
看到傳回值是:
那麼繼續跟進ExtClassLoader(var0):
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
發現進入到了拓展類加載器的一個有參構造器,裡面有一行調用其父類的構造器的代碼:
發現傳進了一個**(ClassLoader)null**,這其實是其父類加載器,繼續跟進super:
繼續跟進super,parent是上回傳進來的null:
再次跟進,parent依然是null:
跟進this,parent是null:
咦!發現指派了:
這個this.parent是ExtClassLoader的一個屬性,它最終被指派為null,印證了上面的說法。
對應用程式類加載器的分析差不多,從下面的Launcher構造器源碼開始:
跟進getAppClassLoader(var1),注意傳值var1是上面建立的拓展類加載器對象:
跟進AppClassLoader(var1x, var0),注意傳值var0是拓展類加載器對象,進入到AppCLassLoader的有參構造器:
跟進super,注意傳值var2是拓展類加載器對象:
跟進super,注意傳值parent是拓展類加載器對象:
跟進super,注意傳值parent是拓展類加載器對象:
跟進this,注意傳值parent是拓展類加載器對象:
到了指派的地方,this.parent被初始化為拓展類加載器。
經過上面的源碼分析,前言中問題1的答案一目了然。
2、自定義類加載器
下面的代碼是一個自定義的類加載器:
package com.jim.jvm.classload;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
private String path; //預設加載路徑
// 初始化自定義類加載器
MyClassLoader(String path) {
// TODO Auto-generated constructor stub
super();
this.path = path;
}
// 重寫ClassLoader的finClass函數(為了加載自定義路徑下的位元組碼檔案)
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
byte[] b = new byte[0];
try {
b = loadData(name);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name, b, 0, b.length);
}
// 讀取自定義路徑的位元組碼檔案
private byte[] loadData(String name) throws IOException {
// 将com.jim.jvm.classload.Test的'.'替換成'/'
name = name.replaceAll("\\.", "/");
// 從自定義類路徑D:/test/com/jim/jvm/classload/Test.class讀取位元組碼檔案
FileInputStream fis = new FileInputStream(this.path + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
接下來測試自定義加載器:
package com.jim.jvm.classload;
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
// 建立自定義類加載器對象
MyClassLoader loader1 = new MyClassLoader("D:/test");
String name = "com.jim.jvm.classload.Test";
loadClassByMyClassLoader(loader1, name);
}
private static void loadClassByMyClassLoader(ClassLoader loader, String name) throws Exception{
Class<?> c = loader.loadClass(name);
// 輸出加載自己這個類的類加載器
System.out.println(c.getClassLoader());
}
}
在測試中,我在D盤建立了一個test檔案夾,然後建立com、jim、jvm和classload檔案夾,将需要加載的位元組碼檔案Test.class放進去。
在程式中設定自定義加載器的加載路徑是D:/test,要加載的檔案是com.jim.jvm.classload.Test,com.jim.jvm.classload這個路徑一定要和正在運作的這個程式所在目錄的路徑一緻。
最後的輸出是:
這樣就成功實作了一個自定義類加載器。
上面自定義類加載器的有參構造器代碼如下,
// 初始化自定義類加載器
MyClassLoader(String path) {
// TODO Auto-generated constructor stub
super();
this.path = path;
}
其中有這行代碼:
我們知道,在面向對象中,一個類在初始化時,會先初始化構造其父類,而我們自定義類加載器的父類是ClassLoader,跟進代碼,跳轉到父類的構造器:
再跟進
進入到:
進入initSystemClassLoader():
我在(2)JVM 類加載之類加載器初始化中貼了一張sum.misc.Launcher類的成員屬性部分的截圖:
那麼執行上一張截圖中這一行代碼
就能擷取到由引導類加載器加載建立的單例sun.misc.Launcher對象。
接下來執行上張圖的
跟進函數getClassLoader():
傳回的是this.loader!!!在上一小節已經分析得到:this.loader是建立的應用程式類加載器,是以這裡傳回的是應用程式類加載器。
src = 應用程式類加載器
再傳回繼續執行上層函數:
上層函數最終也傳回了scl,也就是應用程式類加載器。
再傳回上層調用:
進入執行this:
最後進行指派啦!
這裡的this.parent是自定義類加載器MyClassLoader繼承自ClassLoader的屬性,最終被指派為應用程式類加載器對象,也就是說,自定義類加載器的預設父類加載器是應用程式類加載器。
3、雙親委派邏輯的實作
前面兩小節詳細分析了四種類加載器之間的關系。那麼既然有四種類加載器,在對一個類進行加載時,怎麼判斷由哪個類加載器來加載呢?
JVM的設計者們設計了雙親委派機制。
下面是我自己寫的一個加法類:
package com.jim.jvm.classload;
public class Add {
private int add(int a, int b){
return a + b;
}
public static void main(String[] args) {
int a = 10, b = 20;
Add ad = new Add();
int result = ad.add(a, b);
}
}
在相應地地方加上斷點,并在如下圖需要傳name參數的地方添加debug表達式,以此來過濾其它類(核心類等)的加載過程,直達Add類的加載:
輸入的表達式是:
現在開始debug:
即将進入loadClass函數加載com.jim.jvm.classload.Add:
進入到應用程式類加載器:
這行代碼的功能是尋找應用程式類加載器是否已經加載了Add類,底層實作是本地方法(C++),加載了就指派給c,沒有加載則傳回null。
因為Add類還沒被加載過,是以
c = null
進入下一個斷點執行:
上面這行代碼是調用了應用程式類加載器的父類加載器的loadClass方法,也就是将Add委托給拓展類加載器加載,于是進入下一個斷點執行:
因為拓展類加載器也沒有加載過Add,是以執行結果肯定是
c = null
接下來判斷:
我們知道,拓展類加載器的父類加載器是null,是以會進入執行:
該方法的功能是調用本地方法(C++實作)來查找引導類加載器是否加載過Add。
很明顯沒有加載過,是以:
c = null
進入下一個斷點執行:
findClass(name)在上節的自定義類加載器中也用到過,功能是尋找目前類加載器所能夠加載的目錄下是否有Add。
拓展類加載器的加載目錄是jre.lib.ext,很明顯Add不在此目錄,是以拓展類加載器的loadClass函數最終傳回的c是null。
回到上一層應用程式類加載器的loadClass函數,繼續往下執行,發現c是null,則進入斷點執行:
我們知道,Add就在應用程式類加載器的加載目錄中,是以成功被加載,最終傳回:
c = Add對象
最後将Add的對象指派給了ad變量,整個類加載流程完畢。
到這裡,就能回答前言中的問題2了。
二、雙親委派機制的優缺點
優點:
- 沙箱安全機制。試想一下,引導類加載器加載的是Java的核心類庫,這些類庫肯定是不能輕易被修改的,是以若是我們自己實作了一個List類,在裡面添加了一些新的方法,按照上節的流程,當List被委托到引導類加載器後,發現該同名類已經被加載了,是以就傳回了原來加載的核心類,這樣我們自己寫的List類就不起作用了。
- 避免重複加載。當發現父類加載器發現自己已經加載了某個類時,會直接傳回已經加載過的類的對象,而不會再去委派子類加載器來加載,這樣既避免了重複加載,保證了被加載類的唯一性,也減少了類加載的時間。
缺點:
- 不能同時支援多種不同版本的同名類同時運作。雙親委派機制能夠保護核心類庫,能夠避免重複加載,但這也是它的缺點。要想同時支援多種不同版本的同名類同時運作,比如Tomcat同時運作多個不同版本的Spring項目,就需要打破雙親委派機制,為每個項目都單獨設定一個類加載器來加載,這樣就能隔離同名不同版本的類。這就能夠回答前言中的問題3了。
三、打破雙親委派機制
回答前言中的問題四:雙親委派機制當然能夠打破。
在第一節的第三小節中我們從源碼上分析了雙親委派機制的實作邏輯,也知道每個類加載器的頂級父類是ClassLoader,頂級父類中有兩個核心方法findClass和loadClass。我們在第一節的第二小節中的自定義類加載器裡面重寫過findClass,而雙親委派機制的核心實作在loadClass裡,那想要打破這個機制,就得在自定義類加載器中重寫loadClass,下面是打破雙親委派機制的自定義類加載器代碼:
要重寫loadClass,可以在ClassLoadr中直接把源碼複制過來:
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;
}
}
然後删除雙親委派的部分:
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 (!name.startsWith("com.jim.jvm.classload")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
為什麼呢?因為在加載自己的類之前還需要加載很多其它類庫,比如核心類庫,如果不修改,那麼在自定義的路徑下肯定找不到核心類庫,是以需要将這些類委托給父加載器,也就是引用程式類加載器,然後通過雙親委派的流程來進行加載。
最終得到:
package com.jim.jvm.classload;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
private String path; //預設加載路徑
MyClassLoader(String path) {
// TODO Auto-generated constructor stub
super();
this.path = path;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
byte[] b = new byte[0];
try {
b = loadData(name);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name, b, 0, b.length);
}
@Override
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 t1 = System.nanoTime();
if (!name.startsWith("com.jim.jvm.classload")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private byte[] loadData(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(this.path + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
運作測試:
package com.jim.jvm.classload;
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader loader1 = new MyClassLoader("D:/test");
String name = "com.jim.jvm.classload.Test";
loadClassByMyClassLoader(loader1, name);
}
private static void loadClassByMyClassLoader(ClassLoader loader, String name) throws Exception{
Class<?> c = loader.loadClass(name);
System.out.println(c.getClassLoader());
}
}
最終結果:
可以看到,我在應用程式類加載器的加載目中中也放置了Test類,而我想要加載的是D:/test/com/jim/jvm/classload/Test.class,最終輸出的Test類的加載器也是自定義的類加載器,可見上面的自定義類加載器成功打破了雙親委派機制。
總結
第一次發現肝源碼如此有趣,繼續加油!