JVM虛擬機(jī) 類加載過程與類加載器

類裝載器子系統(tǒng)是JVM中非常重要的部分筛武,是學(xué)習(xí)JVM繞不開的一關(guān)。

一般來說委刘,Java 類的虛擬機(jī)使用 Java 方式如下:

  • Java 源程序(.java 文件)在經(jīng)過 Java 編譯器編譯之后就被轉(zhuǎn)換成 Java 字節(jié)代碼(.class 文件)筑悴。
  • 類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class類的一個(gè)實(shí)例癞季。
  • 每個(gè)這樣的實(shí)例用來表示一個(gè) Java 類劫瞳。
  • 通過此實(shí)例的 newInstance()方法就可以創(chuàng)建出該類的一個(gè)對(duì)象。

類的生命周期#

我們先來看下類的生命周期绷柒,包括:

  • 加載
  • 連接
  • 初始化
  • 使用
  • 卸載

其中加載志于、連接初始化屬于類加載過程废睦。

使用是指我們new對(duì)象進(jìn)行使用伺绽。

卸載指對(duì)象被GC垃圾回收掉。

類加載過程#

JVM的類加載的過程是通過引導(dǎo)類加載器(bootstrap class loader)創(chuàng)建一個(gè)初始類(initial class)來完成的,這個(gè)類是由JVM的具體實(shí)現(xiàn)指定的憔恳。

Class 文件需要加載到虛擬機(jī)中之后才能運(yùn)行和使用瓤荔,系統(tǒng)加載 Class 類型的文件份如下幾步:

  • 加載
  • 連接
    • 驗(yàn)證
    • 準(zhǔn)備
    • 解析
  • 初始

順序是這樣一個(gè)順序,但是加載階段和連接階段部分內(nèi)容是交叉進(jìn)行的钥组,加載階段尚未結(jié)束输硝,連接階段可能就已經(jīng)開始了。

下面我們來逐步解析

加載#

這里的加載微觀上的程梦,是類加載過程中的一小步点把,也是第一步,類加載過程中的加載宏觀上的屿附。

加載的流程如下:

  • 通過全類名獲取定義此類的二進(jìn)制字節(jié)流
  • 將字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
  • 在內(nèi)存中生成一個(gè)代表該類的 Class 對(duì)象郎逃,作為方法區(qū)這些數(shù)據(jù)的訪問入口

簡(jiǎn)單來說就是:加載二進(jìn)制數(shù)據(jù)到內(nèi)存 —> 映射成JVM能識(shí)別的結(jié)構(gòu)—> 在內(nèi)存中生成class文件

虛擬機(jī)規(guī)范上挺份,對(duì)這部分的規(guī)定并不具體褒翰,所以實(shí)現(xiàn)方式是很靈活的。

加載階段我們可以用自定義類加載器去控制字節(jié)流的獲取方式匀泊,是非數(shù)組類的可控性最強(qiáng)的階段优训,而數(shù)組類型不通過類加載器創(chuàng)建,它由 Java 虛擬機(jī)直接創(chuàng)建各聘。

關(guān)于類加載器是什么揣非,后文再聊。

連接#

連接分為三步躲因,驗(yàn)證早敬、準(zhǔn)備、解析大脉,目的是將上面創(chuàng)建好的Class類合并至JVM中搞监,使之能夠執(zhí)行的過程。

驗(yàn)證#

確保class文件中的字節(jié)流包含的信息镰矿,符合當(dāng)前虛擬機(jī)的要求腺逛,保證這個(gè)被加載的class類的正確性,不會(huì)危害到虛擬機(jī)的安全衡怀。

準(zhǔn)備#

為類中的靜態(tài)字段分配內(nèi)存,并設(shè)置默認(rèn)的初始值安疗,比如int類型初始值是0抛杨。

被final修飾的static字段不會(huì)設(shè)置,因?yàn)閒inal在編譯的時(shí)候就分配了荐类。

解析#

解析階段的目的怖现,是將常量池內(nèi)的符號(hào)引用轉(zhuǎn)換為直接引用的過程。

解析動(dòng)作主要針對(duì)類、接口屈嗤、字段潘拨、類方法、接口方法饶号、方法類型等铁追。

如果符號(hào)引用指向一個(gè)未被加載的類,或者未被加載類的字段或方法茫船,那么解析將觸發(fā)這個(gè)類的加載(但未必觸發(fā)這個(gè)類的鏈接以及初始化琅束。)

符號(hào)引用就是一組符號(hào)來描述目標(biāo),可以是任何字面量算谈,符號(hào)引用的字面量形式明確定在《Java 虛擬機(jī)規(guī)范》的Class文件格式中涩禀。

直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄然眼。

舉個(gè)例子:

在程序執(zhí)行方法時(shí)艾船,系統(tǒng)需要明確知道這個(gè)方法所在的位置

Java 虛擬機(jī)為每個(gè)類都準(zhǔn)備了一張方法表來存放類中所有的方法高每。

當(dāng)需要調(diào)用一個(gè)類的方法的時(shí)候屿岂,只要知道這個(gè)方法在方法表中的偏移量就可以直接調(diào)用該方法了。

通過解析操作符號(hào)引用就可以直接轉(zhuǎn)變?yōu)?strong>目標(biāo)方法在類中方法表的位置觉义,從而使得方法可以被調(diào)用雁社。

所以,解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程晒骇,也就是得到類或者字段霉撵、方法在內(nèi)存中的指針或者偏移量

初始化#

初始化就是執(zhí)行類的構(gòu)造器方法洪囤,是類加載的最后一步徒坡,這一步 JVM才開始真正執(zhí)行類中定義的 Java 程序代碼

這個(gè)方法不需要定義,是javac編譯器自動(dòng)收集類中所有類變量的賦值動(dòng)作靜態(tài)代碼塊中的語句合并來的瘤缩。

若該類具有父類喇完,jvm會(huì)保證父類的init()先執(zhí)行,然后在執(zhí)行子類的init()剥啤。

對(duì)于初始化階段锦溪,虛擬機(jī)嚴(yán)格規(guī)范了有且只有 5 種情況下,必須對(duì)類進(jìn)行初始化府怯,只有主動(dòng)去使用類才會(huì)初始化類:

  • 當(dāng)遇到 new 刻诊、 getstaticputstaticinvokestatic 這 4 條直接碼指令時(shí)

    • 當(dāng)遇到一個(gè)類牺丙,讀取一個(gè)靜態(tài)字段(未被 final 修飾)则涯、或調(diào)用一個(gè)類的靜態(tài)方法時(shí)复局。
    • 當(dāng) JVM執(zhí)行 new 指令時(shí)會(huì)初始化類。即當(dāng)程序創(chuàng)建一個(gè)類的實(shí)例對(duì)象粟判。
    • 當(dāng) JVM執(zhí)行 getstatic 指令時(shí)會(huì)初始化類亿昏。即程序訪問類的靜態(tài)變量(不是靜態(tài)常量,常量會(huì)被加載到運(yùn)行時(shí)常量池)档礁。
    • 當(dāng) JVM執(zhí)行 putstatic 指令時(shí)會(huì)初始化類角钩。即程序給類的靜態(tài)變量賦值
    • 當(dāng) JVM執(zhí)行 invokestatic 指令時(shí)會(huì)初始化類事秀。即程序調(diào)用類的靜態(tài)方法彤断。
  • 對(duì)類進(jìn)行反射調(diào)用時(shí),如果類沒初始化易迹,需要觸發(fā)其初始化宰衙。

  • 初始化一個(gè)類,如果其父類還未初始化睹欲,則先觸發(fā)該父類的初始化供炼。

  • 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要定義一個(gè)要執(zhí)行的主類 (包含 main 方法的那個(gè)類)窘疮,虛擬機(jī)會(huì)先初始化這個(gè)類袋哼。

  • MethodHandleVarHandle 可以看作是輕量級(jí)的反射調(diào)用機(jī)制,而要想使用這 2 個(gè)調(diào)用闸衫, 就必須先使用 findStaticVarHandle 來初始化要調(diào)用的類涛贯。

  • 「補(bǔ)充,來自issue745 當(dāng)一個(gè)接口中定義了 JDK8 新加入的默認(rèn)方法(被 default 關(guān)鍵字修飾的接口方法)時(shí)蔚出,如果有這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化弟翘,那該接口要在其之前被初始化。

類加載器#

三大類加載器#

了解了類加載過程后骄酗,我們來看看類加載器稀余。

類加載器(ClassLoader)用來加載 Java 類到 Java 虛擬機(jī)中。

JVM 中內(nèi)置了三個(gè)重要的 ClassLoader趋翻,同時(shí)按如下順序進(jìn)行加載:

  1. BootstrapClassLoader 啟動(dòng)類加載器:最頂層的加載類睛琳,由C++實(shí)現(xiàn),負(fù)責(zé)加載 %JAVA_HOME%/lib目錄下的核心jar包和類或者或被 -Xbootclasspath參數(shù)指定的路徑中的所有類踏烙。
  2. ExtensionClassLoader 擴(kuò)展類加載器:主要負(fù)責(zé)加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類师骗,或被 java.ext.dirs 系統(tǒng)變量所指定的路徑下的jar包。
  3. AppClassLoader 應(yīng)用程序類加載器:面向我們用戶的加載器讨惩,負(fù)責(zé)加載當(dāng)前應(yīng)用classpath下的所有jar包和類辟癌。

除了 BootstrapClassLoader 其他類加載器均由 Java 實(shí)現(xiàn)且全部繼承自java.lang.ClassLoader

類的加載幾乎是由上述3種類加載器相互配合執(zhí)行的,在必要時(shí)步脓,我們還可以自定義類加載器。

需要注意的是,Java虛擬機(jī)對(duì)Class文件采用的是按需加載的方式靴患,也就是說當(dāng)需要使用該類時(shí)才會(huì)將它的Class文件加載到內(nèi)存生成Class對(duì)象仍侥。

雙親委派模型#

概念#

每一個(gè)類都有一個(gè)對(duì)應(yīng)它的類加載器。在加載類的時(shí)候鸳君,是采用的雙親委派模型农渊,即把請(qǐng)優(yōu)求先交給父類處理的一種任務(wù)委派模式。

系統(tǒng)中的類加載器在協(xié)同工作的時(shí)候會(huì)默認(rèn)使用 雙親委派模型 或颊。

雙親委派模型的理論很簡(jiǎn)單砸紊,分為如下幾步:

  • 即在類加載的時(shí)候,系統(tǒng)會(huì)首先判斷當(dāng)前類是否被加載過囱挑。已經(jīng)被加載的類會(huì)直接返回醉顽,否則才會(huì)嘗試加載。

  • 加載的時(shí)候平挑,首先會(huì)把該請(qǐng)求委派給該父類加載器的 loadClass() 處理游添,因此所有的請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器 BootstrapClassLoader 中。

  • 當(dāng)父類加載器無法處理時(shí)通熄,才由自己來處理唆涝。

AppClassLoader的父類加載器為ExtensionClassLoader ExtensionClassLoader 的父類加載器為null唇辨,當(dāng)父類加載器為null時(shí)廊酣,會(huì)使用啟動(dòng)類加載器 BootstrapClassLoader 作為父類加載器。

為什么要使用雙親委派模型#

試想一種情況赏枚,我們?cè)陧?xiàng)目目錄下亡驰,手動(dòng)創(chuàng)建了一個(gè)java.lang 包,并在該包下創(chuàng)建了一個(gè)Object嗡贺,這時(shí)候我們?cè)偃?dòng)Java程序隐解,原生Object會(huì)被篡改嗎?當(dāng)然是不會(huì)的诫睬!

因?yàn)?code>Object類是Java的核心庫類煞茫,由BootstrapClassLoader加載,而自定義的java.lang.Object類應(yīng)該是由AppClassLoader來加載摄凡。

BootstrapClassLoader先于AppClassLoader進(jìn)行加載续徽,根據(jù)上面的雙親委派模型的概念,我們可以知道亲澡,java.lang.Object類已經(jīng)被加載钦扭,并且AppClassLoader要加載類之前都要先給其父類過目,所以自己寫的野類是無法撼動(dòng)核心庫類的床绪。

結(jié)論

雙親委派模型保證了Java程序的穩(wěn)定運(yùn)行客情,可以避免類的重復(fù)加載其弊,也保證了 Java 的核心 API 不被篡改。

源碼分析#

雙親委派模型的都集中在 java.lang.ClassLoaderloadClass() 中膀斋,相關(guān)代碼如下所示:

private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先梭伐,檢查請(qǐng)求的類是否已經(jīng)被加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //父加載器不為空,調(diào)用父加載器loadClass()方法處理
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //父加載器為空仰担,使用啟動(dòng)類加載器 BootstrapClassLoader 加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //拋出異常說明父類加載器無法完成加載請(qǐng)求
                }

                if (c == null) {
                    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;
        }
    }

反雙親委派模型#

雙親委派模型是Java默認(rèn)的糊识,假如我們不想用雙親委派,我們要怎么辦呢摔蓝?

我們可以自定義一個(gè)類加載器赂苗,除了 BootstrapClassLoader 其他類加載器均由 Java 實(shí)現(xiàn)且全部繼承自java.lang.ClassLoader。如果我們要自定義自己的類加載器贮尉,很明顯需要繼承 ClassLoader拌滋。

從上面的源碼我們知道,雙親委派模型的都集中在 java.lang.ClassLoaderloadClass() 中绘盟,如果想打破雙親委派模型則需要重寫 loadClass() 方法鸠真。

如果我們不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法即可龄毡,無法被父類加載器加載的類最終會(huì)通過這個(gè)方法被加載卖擅。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末荒勇,一起剝皮案震驚了整個(gè)濱河市题画,隨后出現(xiàn)的幾起案子篮灼,更是在濱河造成了極大的恐慌,老刑警劉巖路操,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疾渴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡屯仗,警方通過查閱死者的電腦和手機(jī)搞坝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來魁袜,“玉大人桩撮,你說我怎么就攤上這事》宓” “怎么了店量?”我有些...
    開封第一講書人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鞠呈。 經(jīng)常有香客問我融师,道長,這世上最難降的妖魔是什么蚁吝? 我笑而不...
    開封第一講書人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任旱爆,我火速辦了婚禮舀射,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘怀伦。我一直安慰自己后控,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開白布空镜。 她就那樣靜靜地躺著,像睡著了一般捌朴。 火紅的嫁衣襯著肌膚如雪吴攒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評(píng)論 1 305
  • 那天砂蔽,我揣著相機(jī)與錄音洼怔,去河邊找鬼。 笑死左驾,一個(gè)胖子當(dāng)著我的面吹牛镣隶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播诡右,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼安岂,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了帆吻?” 一聲冷哼從身側(cè)響起域那,我...
    開封第一講書人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎猜煮,沒想到半個(gè)月后次员,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡王带,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年淑蔚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愕撰。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刹衫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出盟戏,到底是詐尸還是另有隱情绪妹,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布柿究,位于F島的核電站邮旷,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蝇摸。R本人自食惡果不足惜婶肩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一办陷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧律歼,春花似錦民镜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至畔况,卻和暖如春鲸鹦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背跷跪。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來泰國打工馋嗜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吵瞻。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓葛菇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親橡羞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子眯停,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容