深入理解JVM(六)——類載入器原理

飄揚的紅領巾發表於2017-08-25

    我們知道我們編寫的java程式碼,會經過編譯器編譯成位元組碼檔案(class檔案),再把位元組碼檔案裝載到JVM中,對映到各個記憶體區域中,我們的程式就可以在記憶體中執行了。那麼位元組碼檔案是怎樣裝載到JVM中的呢?中間經過了哪些步驟?常說的雙親委派模式又是怎麼回事?本文主要搞清楚這些問題。

類裝載流程

image

1、載入

載入是類裝載的第一步,首先透過class檔案的路徑讀取到二進位制流,並解析二進位制流將裡面的後設資料(型別、常量等)載入到方法區,在java堆中生成對應的java.lang.Class物件。

2、連線

連線過程又分為3步,驗證、準備、解析

2.1、驗證

驗證的主要目的就是判斷class檔案的合法性,比如class檔案一定是以0xCAFEBABE開頭的,另外對版本號也會做驗證,例如如果使用java1.8編譯後的class檔案要再java1.6虛擬機器上執行,因為版本問題就會驗證不透過。除此之外還會對後設資料、位元組碼進行驗證,具體的驗證過程就複雜的多了,可以專門檢視相關資料去了解。

2.2、準備

準備過程就是分配記憶體,給類的一些欄位設定初始值,例如:

public static int v=1;

這段程式碼在準備階段v的值就會被初始化為0,只有到後面類初始化階段時才會被設定為1。

但是對於static final(常量),在準備階段就會被設定成指定的值,例如:

public static final  int v=1;

這段程式碼在準備階段v的值就是1。

2.3、解析

解析過程就是將符號引用替換為直接引用,例如某個類繼承java.lang.object,原來的符號引用記錄的是“java.lang.object”這個符號,憑藉這個符號並不能找到java.lang.object這個物件在哪裡?而直接引用就是要找到java.lang.object所在的記憶體地址,建立直接引用關係,這樣就方便查詢到具體物件。

3、初始化

初始化過程,主要包括執行類構造方法、static變數賦值語句,staic{}語句塊,需要注意的是如果一個子類進行初始化,那麼它會事先初始化其父類,保證父類在子類之前被初始化。所以其實在java中初始化一個類,那麼必然是先初始化java.lang.Object,因為所有的java類都繼承自java.lang.Object。

說完了類載入過程,我們來介紹一下這個過程當中的主角:類載入器。

類載入器

類載入器ClassLoader,它是一個抽象類,ClassLoader的具體例項負責把java位元組碼讀取到JVM當中,ClassLoader還可以定製以滿足不同位元組碼流的載入方式,比如從網路載入、從檔案載入。ClassLoader的負責整個類裝載流程中的“載入”階段。

ClassLoader的重要方法:

   1:  public Class<?> loadClass(String name) throws ClassNotFoundException

載入並返回一個類。

   1:  protected final Class<?> defineClass(byte[] b, int off, int len)

定義一個類,該方法不公開被呼叫。

   1:  protected Class<?> findClass(String name) throws ClassNotFoundException

查詢類,loadClass的回撥方法

   1:  protected final Class<?> findLoadedClass(String name)

查詢已經載入的類。

系統中的ClassLoader

BootStrap Classloader (啟動ClassLoader)

Extension ClassLoader (擴充套件ClassLoader)

App ClassLoader(應用 ClassLoader)

Custom ClassLoader(自定義ClassLoader)

每個ClassLoader都有另外一個ClassLoader作為父ClassLoader,BootStrap Classloader除外,它沒有父Classloader。

ClassLoader載入機制如下:

timg

自下向上檢查類是否被載入,一般情況下,首先從App ClassLoader中呼叫findLoadedClass方法檢視是否已經載入,如果沒有載入,則會交給父類,Extension ClassLoader去檢視是否載入,還沒載入,則再呼叫其父類,BootstrapClassLoader檢視是否已經載入,如果仍然沒有,自頂向下嘗試載入類,那麼從 Bootstrap ClassLoader到 App ClassLoader依次嘗試載入。

值得注意的是即使兩個類來源於相同的class檔案,如果使用不同的類載入器載入,載入後的物件是完全不同的,這個不同反應在物件的 equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用 instanceof 關鍵字對物件所屬關係的判定結果。

5OEYBOFEI]}0K1EVGA(5V)N

從程式碼上可以看出,首先檢視這個類是否被載入,如果沒有則呼叫父類的loadClass方法,直到BootstrapClassLoader(沒有父類),我們把這個過程叫做雙親模式,

雙親模式的問題

頂層ClassLoader,無法載入底層ClassLoader的類

Java框架(rt.jar)如何載入應用的類?

比如:javax.xml.parsers包中定義了xml解析的類介面
Service Provider Interface SPI 位於rt.jar
即介面在啟動ClassLoader中。
而SPI的實現類,在AppLoader。

這樣就無法用BootstrapClassLoader去載入SPI的實現類。

解決

JDK中提供了一個方法:

   1:  Thread. setContextClassLoader()

用以解決頂層ClassLoader無法訪問底層ClassLoader的類的問題;
基本思想是,在頂層ClassLoader中,傳入底層ClassLoader的例項。

雙親模式的破壞

雙親模式是預設的模式,但不是必須這麼做;
Tomcat的WebappClassLoader 就會先載入自己的Class,找不到再委託parent;
OSGi的ClassLoader形成網狀結構,根據需要自由載入Class。

小結

本文介紹了類載入的流程,以及ClassLoader工作機制,最後分析雙親模式的缺陷,以及如何彌補該缺陷,介紹了tomcat、OSGI如何自定義類載入流程。

 

參考資料:

《實戰Java虛擬機器》 葛一鳴

《深入理解Java虛擬機器(第2版)》 周志明

相關文章