我們知道我們編寫的java代碼,會經過編譯器編譯成字節碼文件(class文件),再把字節碼文件裝載到JVM中,映射到各個內存區域中,我們的程序就可以在內存中運行了。那么字節碼文件是怎樣裝載到JVM中的呢?中間經過了哪些步驟?常說的雙親委派模式又是怎么回事?本文主要搞清楚這些問題。
類裝載流程
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加載機制如下:
自下向上檢查類是否被加載,一般情況下,首先從App ClassLoader中調用findLoadedClass方法查看是否已經加載,如果沒有加載,則會交給父類,Extension ClassLoader去查看是否加載,還沒加載,則再調用其父類,BootstrapClassLoader查看是否已經加載,如果仍然沒有,自頂向下嘗試加載類,那么從 Bootstrap ClassLoader到 App ClassLoader依次嘗試加載。
值得注意的是即使兩個類來源於相同的class文件,如果使用不同的類加載器加載,加載后的對象是完全不同的,這個不同反應在對象的 equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用 instanceof 關鍵字對對象所屬關系的判定結果。
從代碼上可以看出,首先查看這個類是否被加載,如果沒有則調用父類的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如何自定義類加載流程。