java基礎(chǔ)(第三篇)ClassLoader必知必會(huì)

轉(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é)果如下:

參考

對(duì)你若有幫助點(diǎn)個(gè)贊吧

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末诈铛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子墨礁,更是在濱河造成了極大的恐慌幢竹,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恩静,死亡現(xiàn)場(chǎng)離奇詭異焕毫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)驶乾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門咬荷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人轻掩,你說(shuō)我怎么就攤上這事幸乒。” “怎么了唇牧?”我有些...
    開(kāi)封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵罕扎,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我丐重,道長(zhǎng)腔召,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任扮惦,我火速辦了婚禮臀蛛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘崖蜜。我一直安慰自己浊仆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布豫领。 她就那樣靜靜地躺著抡柿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪等恐。 梳的紋絲不亂的頭發(fā)上洲劣,一...
    開(kāi)封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音课蔬,去河邊找鬼囱稽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛二跋,可吹牛的內(nèi)容都是我干的战惊。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼同欠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼样傍!你這毒婦竟也來(lái)了横缔?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤衫哥,失蹤者是張志新(化名)和其女友劉穎茎刚,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撤逢,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡膛锭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蚊荣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片初狰。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖互例,靈堂內(nèi)的尸體忽然破棺而出奢入,到底是詐尸還是另有隱情,我是刑警寧澤媳叨,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布腥光,位于F島的核電站,受9級(jí)特大地震影響糊秆,放射性物質(zhì)發(fā)生泄漏武福。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一痘番、第九天 我趴在偏房一處隱蔽的房頂上張望捉片。 院中可真熱鬧,春花似錦汞舱、人聲如沸伍纫。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)翻斟。三九已至,卻和暖如春说铃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嘹履。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工腻扇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人砾嫉。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓幼苛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親焕刮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子舶沿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348