《深入理解 Java 虛擬機》學習 -- 類加載機制

《深入理解 Java 虛擬機》學習 -- 類加載機制

1. 概述

虛擬機把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存乓搬,并對數(shù)據(jù)進行校驗思犁、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型进肯,這就是虛擬機的類加載機制激蹲。


2. 類加載的時機

2.1 類的生命周期:

加載 --> 連接(驗證 --> 準備 --> 解析)--> 初始化 --> 使用 --> 卸載

其中,加載江掩、驗證学辱、準備、初始化和卸載這五個階段的順序是確定的环形,類的加載過程必須按照這種順序按部就班地開始策泣,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持 Java 語言的運行時綁定抬吟。

2.2 類的初始化情況

有且只有四種情況必須立即對類進行 “初始化”:

  • 遇到 new萨咕、getstatic、putstatic 或 invokestatic 這 4 條字節(jié)碼指令時拗军,如果類沒有進行過初始化任洞,則需要先觸發(fā)其初始化

    場景:

    1. 使用 new 關(guān)鍵字實例化對象
    2. 讀取或設(shè)置一個類的靜態(tài)字段(被 final 修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)
    3. 調(diào)用一個類的靜態(tài)方法的時候
  • 使用 java.lang.reflect 包的方法對類進行反射調(diào)用時发侵,如果類沒有進行過初始化交掏,則需要先觸發(fā)其初始化

  • 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化刃鳄,則需要先觸發(fā)其父類的初始化

  • 當虛擬機啟動時盅弛,用戶需要指定一個要執(zhí)行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類


3. 類加載的過程

3.1 加載

在加載階段叔锐,虛擬機需要完成以下三件事情:

  • 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流
  • 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
  • 在 Java 堆中生成一個代表這個類的 java.lang.Class 對象挪鹏,作為方法區(qū)這些數(shù)據(jù)的訪問入口

3.2 驗證

會完成四個階段的檢驗過程:

  1. 文件格式驗證
  2. 元數(shù)據(jù)驗證
  3. 字節(jié)碼驗證
  4. 符號引用驗證

3.3 準備

準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進行分配愉烙。

幾個注意點:

  • 這時候進行內(nèi)存分配的僅包括類變量(被 static 修飾的變量)讨盒,而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在 Java 堆中(初始化階段)步责。

  • 這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值(如0返顺、0L禀苦、null、false等)遂鹊,而不是被在Java代碼中被顯式地賦予的值振乏。

    舉個例子:

    // 變量 value 在準備階段過后的初始值為 0 而不是 123
    public static int value = 123;
    
  • 特殊情況:如果類字段的字段屬性表中存在 ConstantValue 屬性(即 final 常量),那在準備階段變量 value 就會被初始化為 ConstantValue 屬性所指定的值秉扑。

    舉個例子:

    // 編譯時 Javac 將會為 value 生成 ConstantValue 屬性慧邮,在準備階段虛擬機就會根據(jù) ConstantValue 的設(shè)置將 value 賦值為 123
    public static final int value = 123;
    

3.4 解析

解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。

概念說明:

  • 符號引用:符號引用以一組符號來描述所引用的目標舟陆,符號可以是任何形式的字面量误澳,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān)吨娜,引用的目標并不一定已經(jīng)加載到內(nèi)存中脓匿。
  • 直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄宦赠。直接引用是與虛擬機實現(xiàn)地內(nèi)存布局相關(guān)的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同米母。如果有了直接引用勾扭,那引用的目標必定已經(jīng)在內(nèi)存中存在。

3.5 初始化

類初始化階段是類加載過程的最后一步铁瞒,前面的類加載過程(準備階段)中妙色,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制慧耍。到了初始化階段身辨,才真正開始執(zhí)行類中定義的 Java 程序代碼。

初始化階段是執(zhí)行類構(gòu)造器 <clinit>() 方法的過程

在準備階段芍碧,類變量已經(jīng)賦過一次系統(tǒng)要求的初始值煌珊,而在初始化階段,則是根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源(如類成員變量 -- 除了類變量以外的變量都屬于類成員變量)泌豆。

幾個應(yīng)該記住的概念:

  1. <clinit>() 方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{} 塊)中的語句合并產(chǎn)生的(會優(yōu)先執(zhí)行 static{}

  2. 虛擬機會保證在子類的 <clinit>() 方法執(zhí)行之前定庵,父類的 <clinit>() 方法已經(jīng)執(zhí)行完畢。因此在虛擬機中第一個被執(zhí)行的 <clinit>() 方法的類肯定是 java.lang.Object踪危。

  3. 由于父類的 <clinit>() 方法先執(zhí)行蔬浙,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作。

    舉個例子贞远,下面的代碼中運行結(jié)果 字段 B 的值將會是 2 而不是 1畴博。

    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }
    static class Sub extends Parent {
        public static int B = A;
    }
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
    
  4. <clinit>() 方法對于類或接口來說并不是必須的,如果一個類中沒有靜態(tài)語句塊蓝仲,也沒有對變量的賦值操作俱病,那么編譯器可以不為這個類生成 <clinit>() 方法蜜唾。

  5. 接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作庶艾,因此接口與類一樣都會生成 <clinit>() 方法袁余。


4. 類加載器

4.1. 比較兩個類是否 "相等"

只有在這兩個類是由同一個類加載器加載的前提之下才有意義,否則咱揍,即使這兩個類是來源于同一個 Class 文件颖榜,只要它們的類加載器不同,那這兩個類就必定不相等煤裙。

4.2 類加載器

不同的類加載器(Java 虛擬機角度)
  1. 啟動類加載器(C++ 實現(xiàn)):虛擬機自身一部分
  2. 所有其他的類加載器(Java 實現(xiàn)):獨立于虛擬機外部掩完,繼承自抽象類 java.lang.ClassLoader
不同的類加載器(Java 開發(fā)人員角度)
  1. 啟動類加載器:與上述一致

    負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數(shù)所指定的路徑中硼砰,并且是虛擬機識別的類庫加載到虛擬機內(nèi)存中且蓬。啟動類加載器無法被 Java 程序直接引用。

  2. 擴展類加載器:由 sun.misc.Launcher$ExtClassLoader 實現(xiàn)

    負責加載 <JAVA_HOME>\lin\ext 目錄中的题翰,或者被 java.ext.dirs 系統(tǒng)變量所指定的路徑中的所有類庫恶阴,開發(fā)者可以直接使用擴展類加載器

  3. 應(yīng)用程序類加載器:由 sun.misc.Launcher$AppClassLoader 來實現(xiàn)

    由于這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也稱它為系統(tǒng)類加載器豹障。它負責加載用戶路徑(ClassPath)上所指定的類庫冯事,開發(fā)者可以直接使用這個類加載器,如果應(yīng)該程序中沒有自定義過自己的類加載器血公,一般情況下這個就是程序中默認的類加載器昵仅。


4.3 雙親委派模型(重要)

上圖:

結(jié)構(gòu):

雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當有自己的父類加載器累魔。這里類加載器之間的父子關(guān)系一般不會以繼承的關(guān)系來實現(xiàn)摔笤,而是都使用組合關(guān)系來復用父加載器的代碼。

工作過程:

如果一個類加載器收到了類加載的請求垦写,它首先不會自己先嘗試加載這個類吕世,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此梯澜,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中寞冯,只有當父類加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載晚伙。

實現(xiàn)原理
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 首先吮龄,檢查請求的類是否已經(jīng)被加載過了
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNUll(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父類加載器拋出 ClassNotFoundException
            // 則說明父類加載器無法完成加載請求
        }
        if (c == null) {
            // 在父類加載器無法加載的時候
            // 再調(diào)用本身的 findClass 方法進行類加載
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

先檢查是否已經(jīng)被加載過,若沒有加載則調(diào)用父加載器的 loadClass() 方法咆疗,若父加載器為空則默認使用啟動類加載器作為父加載器漓帚。如果父類加載失敗,則在拋出 ClassNotFoundException 異常后午磁,再調(diào)用自己的 findClass() 方法進行加載尝抖。

Java 中不符合雙親委派模型的例子
  1. 基礎(chǔ)類需要調(diào)用回用戶的代碼

    • 引用上下文類加載器(Thread Context ClassLoader)毡们,這個類加載器可以通過 java.lang.Thread 類的 setContextClassLoaser() 方法進行設(shè)置,如果創(chuàng)建線程時還未設(shè)置昧辽,它將會從父線程中繼承一個衙熔;如果在應(yīng)用程序的全局范圍內(nèi)都沒有設(shè)置過,那么這個類加載器默認就是應(yīng)用程序類加載器搅荞。

    • 因為父類加載器請求子類加載器去完成類加載的動作红氯,實際上是逆向使用類加載器,不符合雙親委派模型咕痛。

    • 如:JDBC

  2. 程序動態(tài)性

    • 如代碼熱替換痢甘,模塊熱部署等。(其實就是模塊化操作)

    • 這種模塊化的模式標準我們稱之為 "OSGi"茉贡。(自定義的類加載器機制的實現(xiàn))

    • 在該標準下塞栅,每一個程序模塊(Bundle)都有一個自己的類加載器,當需要更換一個 Bundle 時腔丧,就把 Bundle 連同類加載器一起換掉以實現(xiàn)代碼的熱替換放椰。

    • 在 OSGi 環(huán)境下,類加載器不再是雙親委派模型中的樹狀結(jié)果悔据,而是進一步發(fā)展為網(wǎng)狀結(jié)構(gòu)庄敛。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市科汗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绷雏,老刑警劉巖头滔,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異涎显,居然都是意外死亡坤检,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門期吓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來早歇,“玉大人,你說我怎么就攤上這事讨勤〖” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵潭千,是天一觀的道長谱姓。 經(jīng)常有香客問我,道長刨晴,這世上最難降的妖魔是什么屉来? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任路翻,我火速辦了婚禮,結(jié)果婚禮上茄靠,老公的妹妹穿的比我還像新娘茂契。我一直安慰自己,他們只是感情好慨绳,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布掉冶。 她就那樣靜靜地躺著,像睡著了一般儡蔓。 火紅的嫁衣襯著肌膚如雪郭蕉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天喂江,我揣著相機與錄音召锈,去河邊找鬼。 笑死获询,一個胖子當著我的面吹牛涨岁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吉嚣,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼梢薪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了尝哆?” 一聲冷哼從身側(cè)響起秉撇,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎秋泄,沒想到半個月后琐馆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡恒序,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年瘦麸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歧胁。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡滋饲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出喊巍,到底是詐尸還是另有隱情屠缭,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布玄糟,位于F島的核電站勿她,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏阵翎。R本人自食惡果不足惜逢并,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一之剧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧砍聊,春花似錦背稼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至俯树,卻和暖如春帘腹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背许饿。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工阳欲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人陋率。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓球化,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瓦糟。 傳聞我的和親對象是個殘疾皇子筒愚,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

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