1. 程式人生 > >深入理解ClassLoader工作機制(jdk1.8)

深入理解ClassLoader工作機制(jdk1.8)

關於 擁有 作用 再次 lin public 雙親委托模型 rap 訪問

ClassLoader 顧名思義就是類加載器,ClassLoader 作用:

負責將 Class 加載到 JVM 中
審查每個類由誰加載(父優先的等級加載機制)
將 Class 字節碼重新解析成 JVM 統一要求的對象格式

類加載時機與過程

類從被加載到虛擬機內存中開始,直到卸載出內存為止,它的整個生命周期包括了:加載、驗證、準備、解析、初始化、使用和卸載這7個階段。其中,驗證、準備和解析這三個部分統稱為連接(linking)。
這裏寫圖片描述

技術分享圖片
其中,加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類的加載過程必須按照這種順序按部就班的“開始”(僅僅指的是開始,而非執行或者結束,因為這些階段通常都是互相交叉的混合進行,通常會在一個階段執行的過程中調用或者激活另一個階段),而解析階段則不一定(它在某些情況下可以在初始化階段之後再開始,這是為了支持Java語言的運行時綁定。

什麽情況下需要開始類加載過程的第一個階段:”加載”。虛擬機規範中並沒強行約束,這點可以交給虛擬機的的具體實現自由把握,但是對於初始化階段虛擬機規範是嚴格規定了如下幾種情況,如果類未初始化會對類進行初始化:

創建類的實例
對類進行反射調用的時候,如果累沒有進行過初始化,則需要先觸發其初始化
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main 方法的那個類),虛擬機會先初始化這個主類
當使用jdk1.7動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。

註意,對於這五種會觸發類進行初始化的場景,虛擬機規範中使用了一個很強烈的限定語:“有且只有”,這五種場景中的行為稱為對一個類進行 主動引用。除此之外,所有引用類的方式,都不會觸發初始化,稱為 被動引用。

特別需要指出的是,類的實例化與類的初始化是兩個完全不同的概念:

類的實例化是指創建一個類的實例(對象)的過程;

類的初始化是指為類中各個類成員(被static修飾的成員變量)賦初始值的過程,是類生命周期中的一個階段。

下面是被動引用的幾個例子:
(1)

/**
* jdk:1.8
* 通過子類引用父類的靜態字段,不會導致子類初始化
*/
class SuperClass{

static {
System.out.println("SuperClass init!");
}
public static int value=123;

}
class SubClass extends SuperClass{

static {
System.out.println("SubClass init!");
}

}

public class Test {
public static void main(String[] args)throws Exception{
System.out.println(SubClass.value);
//輸出:
//SuperClass init!
//123

}
}



通過其子類來引用父類中定義的靜態字段,只會觸發父類初始化而不會觸發子類的初始化。至於是否要觸發子類的加載和驗證,這個在虛擬機規範中並沒有明確規定,這點取決於虛擬機的具體實現。

(2)

/**
* jdk:1.8
* 通過數組定義來引用類,不會觸發此類的初始化
*/
class SuperClass{

static {
System.out.println("SuperClass init!");
}
public static int value=123;

}
class SubClass extends SuperClass{

static {
System.out.println("SubClass init!");
}

}

public class Test {
public static void main(String[] args)throws Exception{
SuperClass[] sca=new SuperClass[10];
//無輸出
}
}



通過數組定義來引用類,不會觸發此類的初始化。

(3)

/**
* jdk:1.8
* 常量在編譯階段會存入調用類的常量池,
* 本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
*/
class ConstClass{
static {
System.out.println("ConstClass init!");
}
public static final String HELLO = "hello";

}

public class Test {
public static void main(String[] args)throws Exception{
System.out.println(ConstClass.HELLO);
//輸出 hello
}
}



常量在編譯階段會存入調用類的常量池,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
ClassLoader 類結構分析

public abstract class ClassLoader {

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;

//...
}



這裏註意下有父加載器,這個我們再後面再來闡述,這裏先有個印象。

以下是 ClassLoader 常用到的幾個方法及其重載方法:
(1)

defineClass(String name, java.nio.ByteBuffer b,ProtectionDomain protectionDomain)


指定保護域(protectionDomain),把ByteBuffer的內容轉換成 Java 類,這個方法被聲明為final的。

(2)

defineClass(String name, byte[] b, int off, int len)



把字節數組 b中的內容轉換成 Java 類,其開始偏移為off,這個方法被聲明為final的。

(3)

//查找指定名稱的類
findClass(String name)


(4)

//加載指定名稱的類
loadClass(String name)


(5)

//鏈接指定的類
resolveClass(Class<?>)



其中 defineClass 方法用來將 字節流解析成 JVM 能夠識別的 Class 對象,有了這個方法意味著我們不僅僅可以通過 class 文件實例化對象,還可以通過其他方式實例化對象,如果我們通過網絡接收到一個類的字節碼,拿到這個字節碼流直接創建類的 Class 對象形式實例化對象。如果直接調用這個方法生成類的 Class 對象,這個類的 Class 對象還沒有 resolve ,這個 resolve 將會在這個對象真正實例化時才進行。

defineClass 通常是和findClass 方法一起使用的,我們通過覆蓋ClassLoader父類的findClass 方法來實現類的加載規則,從而取得要加載類的字節碼,然後調用defineClass方法生成類的Class 對象,如果你想在類被加載到JVM中時就被鏈接,那麽可以接著調用另一個 resolveClass 方法,當然你也可以選擇讓JVM來解決什麽時候才鏈接這個類。
ClassLoader 的等級加載機制

Java默認提供的三個ClassLoader
BootStrap ClassLoader

稱為啟動類加載器,是Java類加載層次中最頂層的類加載器,負責加載JDK中的核心類庫,如:rt.jar、resources.jar、charsets.jar等,這個ClassLoader完全是JVM自己控制的,需要加載哪個類,怎麽加載都是由JVM自己控制,別人也訪問不到這個類,所以BootStrap ClassLoader不遵循委托機制(後面再闡述什麽是委托機制),沒有子加載器。

下面是測試 BootStrap ClassLoader 加載的哪些文件:

/**
* BootStrap ClassLoader 加載的文件
*/
public class Test {
public static void main(String[] args)throws Exception{
System.out.println(System.getProperty("sun.boot.class.path"));
}
}



輸出:

C:\Program Files\Java\jre1.8.0_151\lib\resources.jar;
C:\Program Files\Java\jre1.8.0_151\lib\rt.jar;
C:\Program Files\Java\jre1.8.0_151\lib\sunrsasign.jar;
C:\Program Files\Java\jre1.8.0_151\lib\jsse.jar;
C:\Program Files\Java\jre1.8.0_151\lib\jce.jar;
C:\Program Files\Java\jre1.8.0_151\lib\charsets.jar;
C:\Program Files\Java\jre1.8.0_151\lib\jfr.jar;
C:\Program Files\Java\jre1.8.0_151\classes



EtxClassLoader

稱為擴展類加載器,負責加載Java的擴展類庫,Java 虛擬機的實現會提供一個擴展庫目錄,該類加載器在此目錄裏面查找並加載 Java 類。默認加載JAVA_HOME/jre/lib/ext/目下的所有jar。

/**
* EtxClassLoader 加載文件
*/
public class Test {
public static void main(String[] args)throws Exception{
System.out.println(System.getProperty("java.ext.dirs"));
}
}



輸出:

C:\Program Files\Java\jre1.8.0_91\lib\ext;



AppClassLoader

稱為系統類加載器,負責加載應用程序classpath目錄下的所有jar和class文件。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。我們可以通過System.getProperty(“java.class.path”) 來查看 classpath。

除了引導類加載器(BootStrap ClassLoader)之外,所有的類加載器都有一個父類加載器,對於系統提供的類加載器來說,系統類加載器(如:AppClassLoader)的父類加載器是擴展類加載器(EtxClassLoader),而擴展類加載器的父類加載器是引導類加載器;對於開發人員編寫的類加載器來說,其父類加載器是加載此類加載器 Java 類的類加載器。因為類加載器 Java 類如同其它的 Java 類一樣,也是要由類加載器來加載的。一般來說,開發人員編寫的類加載器的父類加載器是系統類加載器。類加載器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類加載器。
ClassLoader加載類的原理

ClassLoader使用的是雙親委托模型來搜索加載類的
雙親委托模型

ClassLoader使用的是雙親委托機制來搜索加載類的,每個ClassLoader實例都有一個父類加載器的引用(不是繼承的關系,是一個組合的關系),虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它ClassLoader實例的的父類加載器。當一個ClassLoader實例需要加載某個類時,它會試圖親自搜索某個類之前,先把這個任務委托給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,如果沒加載到,則把任務轉交給Extension ClassLoader試圖加載,如果也沒加載到,則轉交給App ClassLoader 進行加載,如果它也沒有加載得到的話,則返回給委托的發起者,由它到指定的文件系統或網絡等URL中加載該類。如果它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。否則將這個找到的類生成一個類的定義,並將它加載到內存當中,最後返回這個類在內存中的Class實例對象。

類加載器雙親委托模型:
這裏寫圖片描述

技術分享圖片
雙親委托模型好處

因為這樣可以避免重復加載,當父親已經加載了該類的時候,就沒有必要 ClassLoader再加載一次。考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況,因為String已經在啟動時就被引導類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,除非你改變JDK中ClassLoader搜索類的默認算法。
類與類加載器

類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以表達更通俗一些:比較兩個類是否”相等”,只有再這兩個類是有同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
類加載器源碼分析
Launcher

public Launcher() {
ExtClassLoader localExtClassLoader;
try {
// 擴展類加載器
localExtClassLoader = ExtClassLoader.getExtClassLoader();
} catch (IOException localIOException1) {
throw new InternalError("Could not create extension class loader", localIOException1);
}
try {
// 應用類加載器
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
} catch (IOException localIOException2) {
throw new InternalError("Could not create application class loader", localIOException2);
}
// 設置AppClassLoader為線程上下文類加載器
Thread.currentThread().setContextClassLoader(this.loader);
// ...

static class ExtClassLoader extends java.net.URLClassLoader
static class AppClassLoader extends java.net.URLClassLoader
}



Launcher初始化了ExtClassLoader和AppClassLoader,並將AppClassLoader設置為線程上下文類加載器。
ExtClassLoader和AppClassLoader都繼承自URLClassLoader,而最終的父類則為ClassLoader,看看它們的類層次:
技術分享圖片


技術分享圖片

初始化AppClassLoader時傳入了ExtClassLoader實例,當我們進入源碼跟蹤,會來到URLClassLoader 中

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();
}
ucp = new URLClassPath(urls, factory);
acc = AccessController.getContext();
}



可以得知,這個ExtClassLoader 作為了AppClassLoader 的parent,在前面ClassLoader 源碼中,我們知道有個parent 字段,這裏就是在初始化這個字段。
雙親委派

要理解雙親委派,可以查看ClassLoader.loadClass方法:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 檢查是否已經加載過
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) {
// 如果父類加載器無法加載,自身才嘗試加載
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;
}
}


先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass() 方法,若父加載器為空,則默認使用啟動類加載器作為父加載器。如果父加載器失敗,再調用自己的findClass 方法進行加載,因此到這裏再次證明了類加載器的過程:
技術分享圖片

總結

關於類加載器網上有很多的文章,記錄下來,也算是一個自己總結的過程,有時候看很多遍,不如實實在在的寫一遍。
參考

深入理解Java 虛擬機
深入分析Java Web技術內幕
深入理解JVM之ClassLoader
詳細深入分析 Java ClassLoader 工作機制

https://blog.csdn.net/u014634338/article/details/81434327

深入理解ClassLoader工作機制(jdk1.8)