深入理解Java虛擬機之——類加載機制

聲明:原創作品,轉載請注明出處http://www.lxweimin.com/p/336a6f7dd413

Java是一門面向對象的語言,萬物皆對象,萬物都可以用一個類來描述。當我們想要描述一個事物的時候,我們會先創建一個.class文件,然后使用的時候只需要在代碼中new下,這樣這個類的實例對象就出來了。接著就可以調用這個對象的各種之前已經定義的方法。那么就有個問題,就是一個.class文件是如何進入到我們的虛擬機中的,進入之后又做了怎樣的處理。其實這一系列過程我們稱之為類加載機制

一.類的生命周期

一個類在虛擬機中完整的生命周期包括以下幾個階段:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)。其中驗證、準備、解析三個部分統稱為連接(Linking)。順序如下圖:


類的生命周期

加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,但是解析階段不一樣,有可能會在初始化之后,這是為了支持Java語言的運行時綁定。
類的加載過程其實只是上面的加載到初始化過程,接下來我們來挨個看下這幾個階段:

二.類的加載過程

1.加載

這一階段虛擬機主要要做的三件事情是:

  • 1.通過此類的全限定名來獲取此類的二進制字節流
    1. 將這個二進制靜態存儲結構轉換為方法區的運行時數據結構
  • 3.在Java堆中生成一個代表這個類的Class對象,作為方法區這些數據的訪問入口

2.驗證

驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。驗證分四個階段:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
a.文件格式驗證
第一階段要驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理。
b.元數據驗證
第二階段是對字節碼描述的信息進行予以分析,以保證其描述的信息符合Java語言規范的要求。
c.字節碼驗證
第三階段是整個驗證過程中最復雜的一個階段,主要工作是進行數據流和控制流分析。在第二階段對元數據信息中的數據類型做完校驗后,這個階段對類的方法體進行校驗分析。這個階段的任務是保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為。
d.符號引用驗證
最后一個階段的校驗發生在虛擬機將符號引用轉為直接引用的時候,這個轉換動作將在連接的第三個階段——解析階段中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進行匹配性的校驗,其目的是確保解析動作能正常執行。

3.準備

準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中進行分配。這里的類變量指的是被static修飾的變量,這里設置的初始值為0值。如下

public static int value  = 123;

那么變量value在準備階段過后其值為0而不是123,當然如果這個value是被final修飾,那么在準備階段會直接設置為123,如下:

public static final int value = 123;

4.解析

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

  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標不一定已經加載到內存中。
  • 直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在。

對同一個符號引用進行多次解析請求是很常見的事情,虛擬機實現可能會對第一次解析的結果進行緩存。解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用進行。

a.類或接口的解析
假設當前代碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,那虛擬機完成整個解析的過程需要包括以下3個步驟:

  • 1.如果C不是一個數組類型,那虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類。在加載過程中,由于無數據驗證、字節碼驗證的需要,又將可能觸發其他相關類的加載動作,例如加載這個類的父類或實現的接口。一旦這個加載過程出現任何異常,解析過程就將宣告失敗。
  • 2.如果C是一個數組類型,并且數組的元素類型為對象,也就是N的描述符會是類似[java.lang.Integer的形式,那將會按照第一點的規則加載數組元素類型。如果N的描述符如前面所假設的形式,需要加載的元素類型就是java.lang.Integer,接著由虛擬機生成一個代表此數組維度和元素的數組對象。
  • 3.如果上面的步驟沒有出現任何異常,那么C在虛擬機中實際上已經成為一個有效的類或接口了,但在解析完成之前還要進行符號引用驗證,確認C是否具備對D的訪問權限。如果發現不具備訪問權限,將拋出java.lang.IllegalAccessError異常。

b.字段解析
在解析字段之前先要解析字段所屬的類或接口,如果解析成功,那將這字段所屬的類或接口用C表示,虛擬機規范要求按照如下步驟對C進行后續字段的解析:

  • 1.如果C本身就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  • 2.否則,如果在C中實現了接口,將會按照繼承關系從上往下遞歸搜索各個接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目標想匹配的字段,則返回這個字段的直接引用,查找結束。
  • 3.否則,如果C不是java.lang.Object的話,將會按照繼承關系從上往下遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  • 4.否則,查找失敗,拋出java.lang.NoSuchFieldError異常。

如果查找過程成功返回了引用,將會對這個字段進行權限驗證,如果發現不具備對字段的訪問權限,將拋出java.lang.IllegalAccessError異常。

c.類方法解析
類方法解析的第一個步驟與字段解析一樣,需要先解析方法所屬的類或接口,如果成功我們依然用C表示這個類,接下來虛擬機將會按照如下步驟進行后續的類方法解析:

  • 1.類方法和接口方法符號引用的常量類型定義是分開的,如果C是一個接口,則直接拋出java.lang.IncompatibleClassChangeError異常。
  • 2.如果通過了上面這步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  • 3.否則,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  • 4 否則,在類C實現的接口列表及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,如果存在相匹配的方法,說明類C是一個抽象類,這個時候查找結束,拋出java.lang.AbstractMethodError異常。
  • 5.否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

最后如果查找過程成功返回了直接引用,將會對這個方法進行權限驗證;如果發現不具備對此方法的訪問權限,將拋出java.lang.IllegalAccessError異常。

d.接口方法解析
同上,解析接口方法時需先解析方法所屬的類或符號的符號引用,如果成功,依然用C表示這個接口,解析步驟如下:

  • 1.與類方法解析相反,如果發現這個C是類不是接口,則直接拋出java.lang.IncompatibleClassChangeError異常。
  • 2.否則,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  • 3.否則,在接口C的父接口中遞歸查找,直到java.lang.Object類為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查找結束。
  • 4.否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

由于接口中的所有方法都默認是public,所以不存在訪問權限的問題,因此接口方法的符號解析應當不會拋出java.lang.IllegalAccessError異常。

5.初始化

類初始化階段是類加載過程的最后一步,之前的階段幾乎都是由虛擬機主導和控制,這一階段才真正執行自己定義的Java程序代碼。初始化階段是執行類構造器<clinit>()方法的過程。來看下<clinit>()方法相關說明:

  • <clinit>()方法是由編譯器自動搜集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合并產生的,編譯器搜集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之后的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。
  • <clinit>()方法與類的構造函數不同,它不需要顯示的調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。因此在虛擬機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object。
  • 由于父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先于子類的變量賦值操作
  • <clinit>()方法對于類或接口來說并不是必須的,如果一個類中沒有靜態語句塊也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。
  • 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法,但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父類接口中定義的變量被使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>()方法。
  • 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖和同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。

3.類加載器

類加載器的功能很簡單,就是通過一個類的全限定名來獲取描述此類的二進制字節流。類加載器雖然只用于實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限于類加載階段。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。

類加載器的分類

類加載分三種:啟動類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)、應用程序類加載器(Application ClassLoader)。其中啟動類加載器是由C++語言實現,是虛擬機自身的一部分,另外兩種均有Java語言實現,獨立于虛擬機外部并且全部繼承自抽象類java.lang.ClassLoader。

  • 啟動類加載器:負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,并且是虛擬機識別的類庫加載到虛擬機內存中。啟動類加載器無法被Java程序直接引用。
  • 擴展類加載器:這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
  • 應用程序類加載器:這個類加載器由sun.msc.Launcher$AppClassLoader來實現。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

雙親委托模型

我們的應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關系如下:


雙親委托模型

上面的這種層次關系,就稱為類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。這里類加載器之間的父類關系一般不會以繼承的關系來實現,而是都使用組合關系來復用父加載器的代碼。
雙親委派模型的工作過程是:如果一個類加載器收到了類加載器的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載。
雙親委派模型實現原理:其代碼實現都在java.lang.ClassLoader的loadClass方法中,首先會檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass方法,若父加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載失敗,則在拋出ClassNotFoundException異常后,再調用自己的findClass方法進行加載。

破壞雙親委派模型

雙親委派模型主要出現過三次被破壞情況
第一次破壞
雙親委派模型是JDK1.2之后才被引入的,需要自定義類加載器時,之前都是直接重寫ClassLoader的loadClass()方法,而引入雙親委派模型后只需要在findClass中實現,所以之前的方式不符合雙親委派模型。
第二次破壞
上面我們提到啟動類加載器都是加載一些API中基礎的類,但是有的時候需要用啟動類加載器加載自己的代碼,這就打破原有的雙親委派模型,比如JNDI服務。
第三次破壞
關于OSGi等熱部署技術,OSGi是一種平級的加載,是一種網狀結構,而不是上述的雙親委派結構,是一種樹狀結構。

參考書籍:《深入理解Java虛擬機》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371