一、類加載時(shí)機(jī)
一個(gè)類型從被加載到虛擬機(jī)內(nèi)存中開始掂榔,到卸載出內(nèi)存為止继效,整個(gè)生命周期將會(huì)經(jīng)歷 加載、驗(yàn)證装获、準(zhǔn)備瑞信、解析、初始化穴豫、使用凡简、卸載 七個(gè)階段。
其中驗(yàn)證精肃、準(zhǔn)備秤涩、解析三個(gè)部分統(tǒng)稱為連接。
六種情況必須立即對(duì)類進(jìn)行“初始化”
- 遇到
new
司抱、getstatic
溉仑、putstatic
或invokestatic
這四條字節(jié)碼指令時(shí),如果類型沒有進(jìn)行初始化状植,則需要先觸發(fā)其初始化階段浊竟。能夠生成這四條指令的典型 Java 代碼場(chǎng)景有:- 使用 new 關(guān)鍵字實(shí)例化對(duì)象時(shí)
- 讀取或設(shè)置一個(gè)類型的靜態(tài)字段(被 final 修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)時(shí)
- 調(diào)用一個(gè)類型的靜態(tài)方法時(shí)
- 使用
java.lang.reflect
包的方法對(duì)類型進(jìn)行反射調(diào)用時(shí)津畸,如果類型沒有進(jìn)行過初始化振定,則需要先觸發(fā)其初始化。 - 當(dāng)初始化類時(shí)肉拓,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化后频,則需要先觸發(fā)其父類的初始化。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí)暖途,用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的類)卑惜,虛擬機(jī)會(huì)先初始化這個(gè)主類。
- 當(dāng)使用 JDK 7 新加入的動(dòng)態(tài)語言支持時(shí)驻售,如果一個(gè)
java.lang.invoke.MethodHandle
實(shí)例最后的解析結(jié)果為REF_getStatic
露久、REF_putStatic
、
REF_invokeStatic
欺栗、REF_newIncokeSpecial
四種類型的方法句柄毫痕,并且這個(gè)方法句柄對(duì)應(yīng)的類沒有進(jìn)行初始化征峦,則需要先觸發(fā)其初始化。 - 當(dāng)一個(gè)接口中定義了 JDK 8 新加入的默認(rèn)方法(被 default 關(guān)鍵字修飾的接口方法)時(shí)消请,如果有這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化栏笆,那該接口要在其之前被初始化。
二臊泰、類加載過程
2.1蛉加、加載
“加載”階段是整個(gè)“類加載”過程中的一個(gè)階段。需要完成三件事情:
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流缸逃。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)七婴。
- 在內(nèi)存中生成一個(gè)代表這個(gè)類的
java.lang.Class
對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口察滑。
加載階段既可以使用 Java 虛擬機(jī)內(nèi)置的引導(dǎo)類加載器來完成,也可以由用戶自定義的類加載器去完成修肠,開發(fā)人員通過自定義的類加載器去控制字節(jié)流的獲取方式(重寫一個(gè)類加載器的findClass()
或loadClass()
方法)贺辰,實(shí)現(xiàn)根據(jù)自己的想法來賦予應(yīng)用程序獲取運(yùn)行代碼的動(dòng)態(tài)性。
數(shù)組類本身不通過類加載器創(chuàng)建嵌施,是由 Java 虛擬機(jī)直接在內(nèi)存中動(dòng)態(tài)構(gòu)造出來的饲化。
加載階段的典型應(yīng)用
- 從 ZIP 壓縮包中讀取,最終發(fā)展出 JAR吗伤、WAR格式吃靠。
- 從網(wǎng)絡(luò)中獲取,典型的是 Web Applet足淆。
- 運(yùn)行時(shí)計(jì)算生成巢块,該場(chǎng)景使用最多的是動(dòng)態(tài)代理技術(shù)。
- 由其他文件生成巧号,如 JSP生成對(duì)應(yīng)的 Class 文件族奢。
- 從數(shù)據(jù)庫中讀取,比較少見丹鸿,如 某些中間件服務(wù)器(SAP Netweaver)越走。
- 從加密文件中獲取,典型的防 Class 文件被反編譯的保護(hù)措施靠欢,通過加載時(shí)解密Class 文件來保障程序運(yùn)行邏輯不被窺探廊敌。
2.2、驗(yàn)證
確保 Class 文件的字節(jié)流中包含的信息符合《Java虛擬機(jī)規(guī)范》的全部約束要求门怪,保證這些信息被當(dāng)做代碼運(yùn)行后不會(huì)危害虛擬機(jī)自身的安全骡澈。
- 文件格式驗(yàn)證
- 元數(shù)據(jù)驗(yàn)證
- 字節(jié)碼驗(yàn)證
- 符號(hào)引用驗(yàn)證
2.3、準(zhǔn)備
正式為類中定義的變量(即靜態(tài)變量掷空,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段秧廉。
- 進(jìn)行內(nèi)存分配的僅包括類變量伞广,而不包括實(shí)例變量(實(shí)例變量隨對(duì)象實(shí)例化時(shí)一起分配在Java堆中)。
- 這里的初始值“通常情況”下是數(shù)據(jù)類型的零值疼电。
public static int value = 123;
// 準(zhǔn)備階段初始值為 0 而非 123嚼锄,賦值123 在初始化階段進(jìn)行
public static final int value = 123;
// 存在 ConstantValue 屬性,準(zhǔn)備階段會(huì)被初始化為 ConstantValue 屬性所指定的初始值蔽豺,這里即 123
2.4区丑、解析
Java 虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程。
具體還包括:
- 類或接口的解析
- 字段解析
- 方法解析
- 接口方法解析
2.5修陡、初始化
直到初始化階段沧侥,Java 虛擬機(jī)才真正開始執(zhí)行類中編寫的Java 程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序魄鸦。
初始化階段就是執(zhí)行類構(gòu)造器 <clinit>()
方法的過程宴杀。
三、類加載器
3.1拾因、雙親委派模型
加載階段旺罢,需要用到類加載器來將 class 文件里面的內(nèi)容搞到 JVM 中生成類對(duì)象。
雙親委派模型用一句話講就是子類加載器先讓父類加載器去查找該類來加載绢记,父類又繼續(xù)請(qǐng)求它的父類直到最頂層扁达,在父類加載器沒有找到所請(qǐng)求的類的情況下,子類加載器才會(huì)嘗試去加載蠢熄,這樣一層一層上去又下來跪解。
每個(gè)類加載器都有固定的查找類的路徑,在JDK8的時(shí)候一共有三種類加載器签孔。
- 啟動(dòng)類加載器(Bootstrap ClassLoader)叉讥,它是屬于虛擬機(jī)自身的一部分,用 C++ 實(shí)現(xiàn)的饥追,主要負(fù)責(zé)加載目錄中或被 -Xbootclasspath 指定的路徑中的并且文件名是被虛擬機(jī)識(shí)別的文件节吮。它是所有類加載器的爸爸。
- 擴(kuò)展類加載器(Extension ClassLoader)判耕,它是 Java 實(shí)現(xiàn)的透绩,獨(dú)立于虛擬機(jī),主要負(fù)責(zé)加載目錄中或被 java.ext.dirs 系統(tǒng)變量所指定的路徑的類庫壁熄。
-
應(yīng)用程序類加載器(Application ClassLoader)帚豪,它是Java實(shí)現(xiàn)的,獨(dú)立于虛擬機(jī)草丧。主要負(fù)責(zé)加載用戶類路徑(classPath)上的類庫狸臣,如果我們沒有實(shí)現(xiàn)自定義的類加載器那這玩意就是我們程序中的默認(rèn)加載器。
JVM_Parents-Delegation-Model
為什么要提出雙親委派模型昌执?
其實(shí)就是為了讓基礎(chǔ)類得以正確地統(tǒng)一地加載烛亦。
從上面的圖可以看出诈泼,如果你也定義了一個(gè) 類,通過雙親委派模式是會(huì)把這個(gè)請(qǐng)求委托給啟動(dòng)類加載器煤禽,它掃描目錄就找到了 jdk 定義的 類來加載铐达,所以壓根不會(huì)加載你寫的 類,這就可以避免一些程序不小心或者有意的覆蓋基礎(chǔ)類檬果。
雖說是子類父類瓮孙,但是加載器之間的關(guān)系不是繼承,而是組合选脊。
public abstract class ClassLoader {
private final ClassLoader parent;
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 {
// 父類是空杭抠,說明是啟動(dòng)類加載器
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;
}
}
}
在 JVM 中,類的唯一性是由類加載器實(shí)例和類的全限定名一同確定的恳啥,也就是說即使是同一個(gè)類文件加載的類偏灿,用不同的類加載器實(shí)例加載,在 JVM 看來這也是兩個(gè)類钝的。
3.2翁垂、破壞雙親委派模型
第一次破壞
在 jdk 1.2 之前,那時(shí)候還沒有雙親委派模型扁藕,不過已經(jīng)有了 ClassLoader 這個(gè)抽象類,所以已經(jīng)有人繼承這個(gè)抽象類疚脐,重寫 loadClass 方法來實(shí)現(xiàn)用戶自定義類加載器亿柑。
而在 1.2 的時(shí)候要引入雙親委派模型,為了向前兼容棍弄, loadClass 這個(gè)方法還得保留著使之得以重寫望薄,新搞了個(gè) findClass 方法讓用戶去重寫,并呼吁大家不要重寫 loadClass 只要重寫 findClass呼畸。
這就是第一次對(duì)雙親委派模型的破壞痕支,因?yàn)殡p親委派的邏輯在 loadClass 上,但是又允許重寫 loadClass蛮原,重寫了之后就可以破壞委派邏輯了卧须。
第二次破壞
第二次破壞指的是 JNDI、JDBC 之類的情況儒陨。
首先得知道什么是 SPI(Service Provider Interface)花嘶,它是面向拓展的,也就是說我定義了個(gè)規(guī)矩蹦漠,就是 SPI 椭员,具體如何實(shí)現(xiàn)由擴(kuò)展者實(shí)現(xiàn)。
像我們比較熟的 JDBC 就是如此笛园。
MySQL 有 MySQL 的 JDBC 實(shí)現(xiàn)隘击,Oracle 有 Oracle 的 JDBC 實(shí)現(xiàn)侍芝,我 Java 不管你內(nèi)部如何實(shí)現(xiàn)的,反正你們這些數(shù)據(jù)庫廠商都得統(tǒng)一按我這個(gè)來埋同,這樣我們 Java 開發(fā)者才能容易的調(diào)用數(shù)據(jù)庫操作州叠,所以在 Java 核心包里面定義了這個(gè) SPI。
而核心包里面的類都是由啟動(dòng)類加載器去加載的莺禁,但它的手只能摸到或Xbootclasspath指定的路徑中留量,其他的它鞭長(zhǎng)莫及。
而 JDBC 的實(shí)現(xiàn)類在我們用戶定義的 classpath 中哟冬,只能由應(yīng)用類加載器去加載楼熄,所以啟動(dòng)類加載器只能委托子類來加載數(shù)據(jù)庫廠商們提供的具體實(shí)現(xiàn),這就違反了自下而上的委托機(jī)制浩峡。
具體解決辦法是搞了個(gè)線程上下文類加載器可岂,通過默認(rèn)情況就是應(yīng)用程序類加載器,然后利用獲得類加載器來加載翰灾。
這就是第二次破壞雙親委派模型缕粹。
第三次破壞:熱部署
這次破壞是為了滿足熱部署的需求,不停機(jī)更新這對(duì)企業(yè)來說至關(guān)重要纸淮,畢竟停機(jī)是大事平斩。
OSGI
就是利用自定義的類加載器機(jī)制來完成模塊化熱部署,而它實(shí)現(xiàn)的類加載機(jī)制就沒有完全遵循自下而上的委托咽块,有很多平級(jí)之間的類加載器查找绘面。
第四次破壞:模塊化
在 JDK9 引入模塊系統(tǒng)之后,類加載器的實(shí)現(xiàn)其實(shí)做了一波更新侈沪。
當(dāng)收到類加載請(qǐng)求揭璃,會(huì)先判斷該類在具名模塊中是否有定義,如果有定義就自己加載了亭罪,沒的話再委派給父類瘦馍。