加載機(jī)制是將Class文件讀取到內(nèi)存,并對數(shù)據(jù)進(jìn)行處理粘舟,最終形成可以被JVM直接使用的Java類型的過程,主要分為加載佩研,鏈接和初始化柑肴,其中鏈接又分為驗(yàn)證(Verify),準(zhǔn)備(Prepare)和解析(Resolve)
加載 Loading
類或接口C的加載過程
- 由相應(yīng)的名稱獲取對應(yīng)的二進(jìn)制字節(jié)流旬薯,可以從rar包中晰骑,網(wǎng)絡(luò)中獲取等
- 將字節(jié)流存儲轉(zhuǎn)換為方法區(qū)的數(shù)據(jù)格式
- 最后實(shí)例化一個(gè)代表該類的Class對象
public static final java.lang.Class XXX
,通過XXX.class
可以訪問到
數(shù)組類沒有外部二進(jìn)制表示绊序,它是由Java虛擬機(jī)直接創(chuàng)建的硕舆,而不是由類加載器創(chuàng)建的秽荞,其他的類型都是使用類加載器加載類或接口的C的二進(jìn)制表示形式
加載器
通過類的全限定名獲取該類的二進(jìn)制字節(jié)流
有兩種類加載器:Java虛擬機(jī)提供的引導(dǎo)類加載器和用戶自定義類加載器。
每個(gè)用戶定義的類加載器都要繼承抽象類ClassLoader
抚官。自定義的類加載器可以加載來自網(wǎng)絡(luò)扬跋,加密文件等的二進(jìn)制字節(jié)流。
在Java中耗式,一個(gè)類是由其完全限定名來標(biāo)識的(包名和類名組成)胁住。 但是一個(gè)類在JVM中的唯一標(biāo)識符號,是完全限定的類名以及加載該類的ClassLoader的實(shí)例組成的刊咳,如果類加載器不同彪见,即使是類型相同,但是Class
類的以下方法返回為false
isInstance(Object obj)// obj是對應(yīng)的Class類型娱挨,等價(jià)于 obj instanceof Class
isAssignableFrom(Class<?> cls) // 對應(yīng)類可以被參數(shù)類型賦值余指,是參數(shù)類本身或者超類
三種系統(tǒng)提供的常用類加載器:
- 啟動類加載器
BootClassLoader
:是虛擬機(jī)的一部分,唯一使用C++實(shí)現(xiàn)的加載器跷坝,負(fù)責(zé)加載$JAVA_HOME/lib
目錄中的rt.jar
等核心類酵镜,以及-Xbootclasspath
指定的路徑中的jar/zip歸檔文件 - 擴(kuò)展類加載器 Extensions Class Loader:加載
$JAVA_HOME/lib/ext
,以及java.ext.dirs
屬性指定路徑匯中的所有類庫柴钻,具體實(shí)現(xiàn)sun.misc.Launcher$ExtClassLoader
- 應(yīng)用程序類加載器
AppClassLoader
:也叫系統(tǒng)類加載器淮韭,加載java.class.path
屬性或者說是-cp / -classpath
命令參數(shù),指定路徑的的所有類庫贴届,具體實(shí)現(xiàn)sun.misc.Launcher$AppClassLoader
靠粪,如果沒有自定義類加載器,這個(gè)就是程序中默認(rèn)的類加載器
ClassLoader
的loadClass
函數(shù)流程:
// 檢查類是否已經(jīng)被加載
Class<?> c = findLoadedClass(name);
//如果沒有被加載毫蚓,調(diào)用父加載器
if (c == null) {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
//如果還沒有找到占键,調(diào)用findClass自己查找
if (c == null)
c = findClass(name);
}
類加載器的層次關(guān)系稱為委派模型,只加載那些在parent中無法加載到的類
線程上下文類加載器元潘,默認(rèn)繼承父線程的上下文加載器畔乙,最初原始線程對應(yīng)的是應(yīng)用類加載器
Thread.currentThread().getContextClassLoader()
破壞雙親委派模型
- 覆蓋掉
loadClass
方法,不讓它先去父類檢驗(yàn)翩概,而改為直接調(diào)用findClass
方法去加載字節(jié)的類 - 加載SPI實(shí)現(xiàn)類
JDK中提供了一個(gè)ServiceLoader<T>
的一個(gè)服務(wù)加載器來實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)牲距,掃描META-INF/services/service-name
文件,讀取其中實(shí)現(xiàn)服務(wù)的具體類名稱
這里需要JDK定義的ServiceLoader
類需要去加載對應(yīng)的服務(wù)提供類的時(shí)候钥庇,雙親委派就無法實(shí)現(xiàn)了
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
這里采用的就是將加載任務(wù)交給線程上下文加載器進(jìn)行加載
- OSGi動態(tài)模塊系統(tǒng)
OSGi 中的每個(gè)模塊(bundle
)都包含 Java包和類牍鞠,模塊可以聲明它所依賴的需要導(dǎo)入的其它模塊的類(通過 Import-Package),也可以聲明導(dǎo)出自己的包和類上沐,供其它模塊使用(通過 Export-Package)皮服,也就是說需要能夠隱藏和共享一個(gè)模塊中的某些 Java 包和類
OSGi 中的每個(gè)模塊都有對應(yīng)的一個(gè)類加載器楞艾,負(fù)責(zé)加載模塊自己包含的類参咙,整體是一個(gè)網(wǎng)狀結(jié)構(gòu)龄广,當(dāng)它需要加載 Java 核心庫的類時(shí)(以java
開頭),它會代理給父類加載器(通常是啟動類加載器)來完成蕴侧,也可以通過org.osgi.framework.bootdelegation
顯式聲明某些 Java 包和類择同,必須由父類加載器來加載。當(dāng)它需要加載所導(dǎo)入的 Java 類時(shí)净宵,它會代理給導(dǎo)出此 Java 類的模塊來完成加載
當(dāng)需要更換一個(gè)bundle時(shí)敲才,就把Bundle連同類加載器一起換掉實(shí)現(xiàn)代碼的熱替換
鏈接 Linking
鏈接包括驗(yàn)證(Verify),準(zhǔn)備(Prepare)類或接口择葡,以及對應(yīng)的父類紧武,父接口,元素類型(數(shù)組類型時(shí))敏储,解析(Resolution)符號引用是鏈接部分的可選操作
驗(yàn)證確保接口或類的二進(jìn)制表示的結(jié)構(gòu)正確阻星,對于主版本號大于50的Class文件,使用類型檢查(Type Checking已添,和Code屬性表的StackMapTable
屬性相關(guān))規(guī)則進(jìn)行驗(yàn)證妥箕,之前采用的是類型推斷(Type Inference)
準(zhǔn)備階段涉及創(chuàng)建靜態(tài)字段并初始化為默認(rèn)值(null,false,0等),在初始化階段才會進(jìn)行靜態(tài)字段的顯式初始化更舞,如果字段屬性表中存在ConstantValue
屬性畦幢,那在準(zhǔn)備階段就初始化為對應(yīng)的值
解析是將運(yùn)行時(shí)常量池的符號引用,動態(tài)替換為具體值的過程缆蝉,除了invokedynamic
指令宇葱,其他指令解析一次可以重復(fù)使用。解析動作主要針對類或接口返奉,字段贝搁,方法,接口方法芽偏,方法類型和方法句柄雷逆,調(diào)用點(diǎn)限定符7類的符號引用進(jìn)行。
初始化 Initialization
初始化階段執(zhí)行類或接口的<clinit>函數(shù)
接口或者類C進(jìn)行初始化的觸發(fā)條件:
- 執(zhí)行new,getstatic, putstatic, 或者 invokestatic四條 JVM 指令中至少一條
- 調(diào)用
java.lang.invoke.MethodHandle
污尉,動態(tài)語言支持相關(guān) - 調(diào)用反射方法膀哲,例如
Class
類和java.lang.reflect
包內(nèi)的方法 - 初始化C的子類
- 接口C含有非靜態(tài)非抽象的方法(默認(rèn)方法)時(shí),初始化實(shí)現(xiàn)接口的類被碗,對應(yīng)接口C進(jìn)行初始化
- C類是虛擬機(jī)啟動時(shí)要執(zhí)行的主類(main函數(shù)對應(yīng)的類)
通過子類引用父類的靜態(tài)字段某宪,不會導(dǎo)致子類初始化
通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化
常量在編譯階段會存入調(diào)用類的常量池中锐朴,本質(zhì)上沒有直接引用到定義常量的類兴喂,因此不會觸發(fā)定義常量的類的初始化
<init> :實(shí)例初始化方法,名字是一個(gè)無效的標(biāo)識符,程序不能提供衣迷,只能由編譯器提供
<clinit>:類或接口的初始化方法畏鼓,最多有一個(gè)該方法,參數(shù)為空壶谒。該方法是類中所有靜態(tài)變量和靜態(tài)塊語句合并產(chǎn)生的云矫,先執(zhí)行父類的<clinit>方法,同時(shí)在執(zhí)行該方法的時(shí)候會上鎖汗菜,多個(gè)線程初始化時(shí)让禀,只有一個(gè)線程執(zhí)行