轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處:
原文鏈接:www.reibang.com/p/92c27f117edc
原文作者:Coder_Ring
本文篇幅稍長(zhǎng),建議收藏慢慢看谷徙。
虛擬機(jī)類加載機(jī)制經(jīng)常在面試的時(shí)候是必考的問(wèn)題讶请,了解類加載機(jī)制對(duì)每個(gè)java程序員來(lái)說(shuō)都是很重要的,知根知底地寫代碼灵寺,遇到問(wèn)題的時(shí)候才能快速找出問(wèn)題所在民效。
虛擬機(jī)類加載過(guò)程
java是一門解釋型語(yǔ)言,虛擬機(jī)在運(yùn)行時(shí)將字節(jié)碼進(jìn)行動(dòng)態(tài)加載和鏈接炬守。一個(gè)類從被加載到內(nèi)存開(kāi)始牧嫉,到它卸載分7個(gè)階段:
- 加載(loading)
- 驗(yàn)證(verification)
- 準(zhǔn)備(preparation)
- 解析(resolution)
- 初始化(initialization)
- 使用(using)
- 卸載(unloading)
其中,加載劳较、驗(yàn)證驹止、準(zhǔn)備浩聋、解析和初始化是類加載的過(guò)程观蜗。解析階段和初始化階段的時(shí)間先后并沒(méi)有強(qiáng)制要求。
加載
通過(guò)類的全限定名衣洁,從指定來(lái)源加載二進(jìn)制字節(jié)碼墓捻,并轉(zhuǎn)換成jvm可以使用的數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中生成一個(gè)代表該類的java.lang.Class對(duì)象坊夫。
這里的來(lái)源可以是文件砖第、網(wǎng)絡(luò)、流环凿、數(shù)據(jù)庫(kù)等梧兼。
驗(yàn)證
對(duì)字節(jié)碼進(jìn)行驗(yàn)證,比如是否符合jvm對(duì)于字節(jié)碼的規(guī)范智听,以及對(duì)安全性等做檢驗(yàn)羽杰。
準(zhǔn)備
- 正式為類變量分配內(nèi)存并設(shè)置初始值。
- 并且只是設(shè)置類靜態(tài)變量的初始值,實(shí)例變量在實(shí)例初始化時(shí)進(jìn)行賦值到推,
- 這里的初始賦值是賦零值考赛,比如boolean 就是設(shè)置為false,int 設(shè)置為 0莉测,float 設(shè)置為0.0f等
- 對(duì)于
public static int value = 666
這種情況也是賦零值颜骤,value 賦值 為666 的操作是在類的初始化階段進(jìn)行 - 如果類字段的字段屬性表中存在ConstantValue屬性,即同時(shí)被final和static修飾的帶賦值的變量捣卤,那么在準(zhǔn)備階段忍抽,變量value就會(huì)被初始化為ConstValue屬性所指定的值。
- 例如:
public static final int value = 666
,編譯時(shí)Javac將會(huì)為value生成ConstantValue屬性董朝,并且這個(gè)屬性的值為666,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù)ConstantValue的設(shè)置將value賦值為666梯找。
解析
將字節(jié)碼常量池中的符號(hào)引用轉(zhuǎn)化為直接引用的過(guò)程。(這里的常量池以及后面提到的常量池益涧,不是指內(nèi)存中的常量池锈锤,而是指class文件中的某一段,你可以理解成字節(jié)碼文件中存放各類信息的倉(cāng)庫(kù),供jvm讀染妹狻)
java源代碼在進(jìn)行javac編譯成字節(jié)碼文件的時(shí)候浅辙,并沒(méi)有像c/c++那樣,在編譯時(shí)進(jìn)行鏈接阎姥,而是在運(yùn)行時(shí)動(dòng)態(tài)鏈接记舆,因此,class字節(jié)碼中并沒(méi)有保存各個(gè)變量呼巴、方法在內(nèi)存中的地址布局泽腮,由虛擬機(jī)動(dòng)態(tài)分配所需內(nèi)存,然后將這些符號(hào)引用解析成如下幾種直接引用:
- 直接指向目標(biāo)的指針(比如衣赶,指向“類型”【Class對(duì)象】诊赊、類變量、類方法的直接引用可能是指向方法區(qū)的指針)
- 相對(duì)偏移量(比如府瞄,指向?qū)嵗兞勘贪酢?shí)例方法的直接引用都是偏移量)
- 一個(gè)能間接定位到目標(biāo)的句柄
直接引用是和虛擬機(jī)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用在不同的虛擬機(jī)實(shí)例上翻譯出來(lái)的直接引用一般不會(huì)相同遵馆。如果有了直接引用鲸郊,那引用的目標(biāo)必定已經(jīng)被加載入內(nèi)存中了。
解析主要針對(duì)的是類或接口货邓、類的字段秆撮、類的方法、接口的方法换况、方法類型职辨、方法句柄和調(diào)用點(diǎn)限定符等符號(hào),下面列出常量池中的符號(hào)與目標(biāo)的對(duì)應(yīng)關(guān)系(大概知道有這個(gè)東西就行复隆,當(dāng)然不止這些拨匆,我懶得寫而已):
初始化
類加載過(guò)程的最后一個(gè)階段,準(zhǔn)備階段的時(shí)候挽拂,變量已經(jīng)賦值過(guò)一次初始值惭每,在初始化階段,代碼中通過(guò)主觀計(jì)劃初始化類變量亏栈,和其他資源台腥,初始化階段就是執(zhí)行類構(gòu)造器方法(<clinit>()方法)的過(guò)程。
什么是<clinit>()?
<clinit>()方法是編譯器自動(dòng)收集類中的所有類靜態(tài)變量的賦值動(dòng)作(比如
public static int value = 666
中的666賦值)和靜態(tài)初始化塊(static代碼塊)的語(yǔ)句合并產(chǎn)生的方法绒北。
- 編譯器收集這些動(dòng)作的順序是根據(jù)語(yǔ)句在源文件中的編寫順序決定的黎侈,因此你如果按以下順序?qū)懘a是無(wú)法通過(guò)編譯的。
static {
yu = 6 ;//給變量賦值可以通過(guò)編譯闷游,
int ss = yu; //但是這句峻汉,引用變量是不能通過(guò)編譯的
}
public static int yu = 8 ;
- <clinit>()方法在繼承關(guān)系中的執(zhí)行順序是先執(zhí)行父類的<clinit>()方法贴汪,這樣就使得父類的靜態(tài)代碼塊先執(zhí)行于子類的靜態(tài)代碼塊。
- <clinit>()方法對(duì)于類和接口來(lái)說(shuō)不是必須的休吠,如果一個(gè)類沒(méi)有靜態(tài)代碼塊扳埂,也沒(méi)有靜態(tài)變量的賦值操作,那么編譯器就不會(huì)生成此方法瘤礁。
- 接口中雖然不能有靜態(tài)初始化塊阳懂,但是仍然可以有類靜態(tài)變量的初始化賦值,因?yàn)榻涌谥卸x的變量都是
public static final
的變量,所以柜思,如果接口中定義了變量岩调,編譯器也會(huì)生產(chǎn)<clinit>()方法。 - 執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父類的<clinit>()方法赡盘,只有當(dāng)使用了父接口中定義的變量時(shí)号枕,才需要執(zhí)行父類的<clinit>()方法,這點(diǎn)和類有所不同亡脑。
- 類只有final修飾的靜態(tài)變量且沒(méi)有static塊堕澄,不會(huì)生成<clinit>()方法邀跃。
- 類的<clinit>()方法在多線程中是線程安全的霉咨,是通過(guò)加鎖同步阻塞實(shí)現(xiàn)線程安全,因此最好不要在一個(gè)類的<clinit>()方法中執(zhí)行耗時(shí)操作拍屑,這樣會(huì)導(dǎo)致多個(gè)線程阻塞途戒。
初始化時(shí)機(jī):
- 創(chuàng)建某個(gè)類的新實(shí)例時(shí):new、反射僵驰、克隆或反序列化喷斋;
- 調(diào)用某個(gè)類的靜態(tài)方法時(shí);以及使用某個(gè)類或接口的靜態(tài)字段或?qū)υ撟侄钨x值時(shí)(final字段除外)蒜茴;
- 調(diào)用Java的某些反射方法時(shí)
- 初始化某個(gè)類的子類時(shí)
- 在虛擬機(jī)啟動(dòng)時(shí)某個(gè)含有main()方法的那個(gè)啟動(dòng)類星爪。
以下情況不會(huì)對(duì)類進(jìn)行初始化:
- 定義數(shù)組時(shí):比如
MyClass[] mc = new MyClass[10];
,這種情況不會(huì)對(duì)MyClass類進(jìn)行初始化粉私。 - 調(diào)用的靜態(tài)方法或者使用的靜態(tài)變量是繼承自父類:比如
int value = Sub.value;
value 是Sub類繼承自SuperClass類的一個(gè)靜態(tài)變量顽腾,不會(huì)觸發(fā)對(duì)Sub類的初始化,反而會(huì)觸發(fā)父類SuperClass的初始化诺核。 - 引用final 修飾的靜態(tài)變量抄肖,且該變量在編譯時(shí)就確定下來(lái),即在聲明處已經(jīng)賦值窖杀,而不是在靜態(tài)初始化塊賦值漓摩。例如:
int value = Sub.value2;
,聲明處為public static final int value2 = 233;
入客,不會(huì)觸發(fā)Sub類的初始化管毙。
類加載器概念
虛擬機(jī)將類的加載過(guò)程中獲取字節(jié)碼的工作交給類加載器來(lái)完成腿椎。
類加載器種類
- 啟動(dòng)類加載器Bootstrap ClassLoader:加載JRE_HOME/lib下的核心包,該類加載器是用c++寫的夭咬。
- 擴(kuò)展類加載器Extension ClassLoader:加載JRE_HOME/lib/ext目錄下的擴(kuò)展包,也可以通過(guò)啟動(dòng)參數(shù)-Djava.ext.dirs指定酥诽,該類用java編寫。對(duì)應(yīng)ExtClassLoader類
- 應(yīng)用類加載器Application ClassLoader:應(yīng)用類加載器皱埠,加載classpath下的字節(jié)碼文件肮帐,用java編寫,對(duì)應(yīng)AppClassLoader這個(gè)類边器,可以通過(guò)ClassLoader類的靜態(tài)方法getSystemClassLoader()獲得训枢,所以又叫系統(tǒng)類加載器
- 自定義類加載器:自定義的類加載器,通過(guò)直接或者間接繼承抽象的ClassLoader類忘巧。
除啟動(dòng)類加載器外恒界,其他類加載器都是直接或者間接地繼承了ClassLoader這個(gè)抽象類。
父子關(guān)系如下:
說(shuō)明:通過(guò)ClassLoader的getParent()方法可以獲得父類加載器砚嘴,但這種父子關(guān)系并不是繼承關(guān)系十酣,是通過(guò)組合實(shí)現(xiàn)的,如果不知道組合是什么可以看一下這篇文章《繼承與組合》际长。
ClassLoader的主要方法
-
public Class<?> loadClass(String name) throws ClassNotFoundException
:公有調(diào)用加載類的入口耸采,內(nèi)部是調(diào)用了loadClass(String name,boolean resolve)
方法 -
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
:通過(guò)雙親委托機(jī)制實(shí)現(xiàn)的加載類的實(shí)現(xiàn)。 -
protected Class<?> findClass(String name) throws ClassNotFoundException
查找類的方法工育,自定義類加載器推薦重寫此方法虾宇。 -
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
:將二進(jìn)制字節(jié)碼轉(zhuǎn)換成Class的一個(gè)實(shí)例。
推薦自己看一下ClassLoader的源碼如绸,可以利用IDE的一些快捷鍵方便自己嘱朽,通過(guò)debug單步執(zhí)行,這樣查看會(huì)清晰很多怔接。
雙親委派機(jī)制
介紹:
如果一個(gè)類加載器收到加載類的請(qǐng)求,它是首先交給父加載器完成加載搪泳,依次委托直到頂層類加載器BootStrap ClassLoader,如果不是該類加載器的職責(zé),它會(huì)交給下層去加載扼脐,依次往下直到找不到合適的類加載器就拋出ClassNotFoundException異常岸军。
優(yōu)點(diǎn):
- 使得類加載不會(huì)重復(fù)加載。
- 具有優(yōu)先級(jí)層次關(guān)系谎势,使得java程序穩(wěn)定運(yùn)作凛膏。
- 舉個(gè)栗子,如果我們使用了一個(gè)自定義的類全限定名一樣的Object類脏榆,雖然編譯不會(huì)報(bào)錯(cuò)猖毫,但是運(yùn)行時(shí)就會(huì)報(bào)錯(cuò),是因?yàn)檫@種雙親委派機(jī)制的存在须喂,即使自定義Object吁断,也會(huì)委托給最頂層的啟動(dòng)類加載器趁蕊,該類加載器發(fā)現(xiàn)這個(gè)類不合法,拋出運(yùn)行時(shí)異常仔役。倘若不是雙親委托掷伙,而是系統(tǒng)類加載器或者自定義類加載器加載Object,因?yàn)榧虞d過(guò)的類會(huì)緩存又兵,會(huì)導(dǎo)致其他繼承了Object類的類受到巨大影響任柜。
破壞雙親委派機(jī)制
雙親委派機(jī)制并不是強(qiáng)制規(guī)范,是可以破壞的沛厨,有些場(chǎng)景是需要破壞它的這種委托機(jī)制的宙地。比如某些情況基于特定要求重寫了loadClass方法。又比如一個(gè)經(jīng)典的情況:
//1
Class.forName("com.mysql.jdbc.Driver");
//2.
DriverManager.getConnection(url);
1和2這兩句代碼相信你再熟悉不過(guò)了逆皮,1是通過(guò)類全限定名去加載mysql的驅(qū)動(dòng)宅粥,一般自定義類是由AppClassLoader加載,而2是由Bootstrap ClassLoader去加載电谣,那么它怎么獲得mysql實(shí)現(xiàn)的類的實(shí)例的秽梅?原來(lái),在DriverManager內(nèi)部實(shí)現(xiàn)中剿牺,不是使用啟動(dòng)類加載器去加載企垦,而是使用調(diào)用了DriverMannager.getConnection方法的類的當(dāng)前線程類加載器去加載,默認(rèn)的當(dāng)前線程上下文類加載器是AppClassLoader,也就是使用了AppClassLoader加載mysql實(shí)現(xiàn)類牢贸。這給我們有所啟發(fā)竹观,可以修改當(dāng)前線程上下文類加載器镐捧,然后用當(dāng)前線程上下文類加載器去加載具體實(shí)現(xiàn)類潜索,實(shí)現(xiàn)破壞雙親委派機(jī)制,這在SPI(服務(wù)提供者接口懂酱,Service Provider Interface)中很常見(jiàn)竹习。
//修改的方法。
Thread.currentThread().setContextClassLoader(classLoader);
類加載器主要應(yīng)用
- 熱部署(Hotswap)列牺,或者有些時(shí)候也叫動(dòng)態(tài)替換整陌、熱替換、熱更新,我們知道修改jsp文件之后瞎领,可以不需要重新啟動(dòng)Tomcat泌辫,實(shí)際上它就是自定義了一個(gè)類加載器實(shí)現(xiàn)對(duì)字節(jié)碼的熱更新。
- 對(duì)字節(jié)碼進(jìn)行加密防止破解
- ......
下面通過(guò)一個(gè)小demo九默,介紹一下怎么自定義類加載器震放,并通過(guò)它加載類。
package wyn.test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class DemoTest {
@Test
public void classLoaderTest() throws Exception {
List list = (List) new MyClassLoader().loadClass("wyn.test.MyList").newInstance();
System.out.println("list.size = " + list.size() + "\nClassLoader:" + list.getClass().getClassLoader());
}
}
class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path path = Paths.get("D:\\" + name.replaceAll("\\.","\\\\") + ".class");
byte[] classData = null;
try {
classData = Files.readAllBytes(path);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name,classData,0,classData.length);
}
}
//MyList.java
package wyn.test;
import java.util.*;
public class MyList extends ArrayList{
@Override
public int size() {
return 666;
}
}
將MyList.java放到D盤驼修,然后cmd下面執(zhí)行javac -d . MyList.java
編譯殿遂,然后就可以通過(guò)IDE執(zhí)行上面的DemoTest的classLoaderTest方法:
運(yùn)行結(jié)果如下:
參考
- blog.csdn.net/u014656992/article/details/51107127
- www.reibang.com/p/639f430fe15a
- www.ibm.com/developerworks/cn/java/j-lo-clobj-init/
- 《深入理解java虛擬機(jī)》
對(duì)你若有幫助點(diǎn)個(gè)贊吧