JVM解析——類加載

本系列主要記錄筆者在學習 [深入理解Java虛擬機] 一書時的理解
我們都知道在Java中,我們并不需要過多的在意內存的管理,這一切都交給了虛擬機自動管理,我們并不需要操心何時需要去釋放一個對象的內存。
當然,如果出現(xiàn)了內存溢出或泄漏,我們就必須去了解一下Java虛擬機的內存管理機制以便于我們解決問題
[筆者仍為Android初學者。如有解釋錯誤的地方,歡迎評論區(qū)指正探討]

本篇為該系列第五篇,深入了解Jvm的類加載機制。


類加載

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

類加載的過程

類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統(tǒng)稱為連接(Linking)。

類加載流程

在整個過程中,加載,驗證,準備,初始化和卸載這五個步驟的開始順序是固定這樣的。而解析則不一定,他在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定。
PS:這里的順序指的是開始順序,而不是進行或完成,是因為這些階段通常都是互相交叉地混合式進行的,通常會在一個階段執(zhí)行的過程中調用、激活另外一個階段。

Java虛擬機規(guī)范中并沒有進行強制約束什么情況下需要開始第一個階段加載,這部分可以交由各個虛擬機的具體實現(xiàn)控制,而對于初始化階段則不同,虛擬機規(guī)范則是嚴格規(guī)定了有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):

  1. 遇到newgetstaticputstaticinvokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態(tài)字段(被final修飾、已在編譯期把結果放入常量池的靜態(tài)字段除外)的時候,以及調用一個類的靜態(tài)方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。
  3. 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化
  4. 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類
  5. 當使用JDK 1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發(fā)其初始化。

這5種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發(fā)初始化,稱為被動引用
列舉一些被動引用的例子:

/**
*通過子類引用父類的靜態(tài)字段,不會導致子類初始化
**/
public class SuperClass{
    public static int value=123;
    static{
        System.out.println("SuperClass init!");
    }
}

public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }
}

/**
*非主動使用類字段演示
**/
public class MainClass {
    public static void main(String[]args){
        System.out.println(SubClass.value);
    }
}

在HotSpot虛擬機的默認情況下:
上述代碼運行之后,只會輸出“SuperClass init!”,而不會輸出“SubClass init!”。
對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段,只會觸發(fā)父類的初始化而不會觸發(fā)子類的初始化。

/**
*通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化
**/
public class SuperClass{
    static{
        System.out.println("SuperClass init!");
    }
}

public class MainClass{
    public static void main(String[]args){
        SuperClass[]sca=new SuperClass[10];
    }
}

通過數(shù)組定義來引用類,并不會觸發(fā)此類的初始化,當卻會觸發(fā)另一個名為[LpackageName.SuperClass的初始化,有沒有覺得個類很熟悉,這不就是我們直接打印一個數(shù)組時出來的東西嗎?它是一個由虛擬機自動生成的、直接繼承于java.lang.Object的子類,創(chuàng)建動作由字節(jié)碼指令newarray觸發(fā)。這個類代表了一個元素類型為SuperClass的一維數(shù)組,數(shù)組中應有的屬性和方法(用戶可直接使用的只有被修飾為publiclength屬性和clone()方法)都實現(xiàn)在這個類里。Java語言中對數(shù)組的訪問比C/C++相對安全是因為這個類封裝了數(shù)組元素的訪問方法。

/**
*被動使用類字段演示三:
*常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類的初始化。
**/
public class ConstClass {
    public static final String HELLOWORLD="hello world";
    static{
        System.out.println("ConstClass init!");
    }
}

/**
*非主動使用類字段演示
**/
public class MainClass {
    public static void main(String[]args){
        System.out.println(ConstClass.HELLOWORLD);
    }
}

上述代碼運行之后,也沒有輸出“ConstClass init!”,這是因為雖然在Java源碼中引用了ConstClass類中的常量HELLOWORLD,但其實在編譯階段通過常量傳播優(yōu)化,已經(jīng)將此常量的值“hello world”存儲到了MainClass類的常量池中,以后MainClass對常量ConstClass.HELLOWORLD的引用實際都被轉化為MainClass類對自身常量池的引用了。

接口的加載和類加載大致相同,一個比較大的區(qū)別在于,類加載要求父類都加載過才可加載子類,而接口并不需要父接口都加載過,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。

大致了解完類加載的流程順序之后,我們來逐步了解每一步都做了什么。

加載

加載,是類加載的第一個過程,在加載階段,虛擬機會完成一下3件事情:

  1. 通過類的全限定名來獲取定義此類的的二進制字節(jié)流。
  2. 將這個字節(jié)流所代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時存儲結構
  3. 在內存中生成一個代表這個類的Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。

對于開發(fā)人員來說,這一步驟是相當自由,虛擬機只限制了要通過一個類的全限定名來獲取此類的二進制字節(jié)流,并沒有限制,這部分流應該從哪獲取。

對于一個非數(shù)組類的加載階段,開發(fā)人員即可以使用系統(tǒng)提供的類加載器去完成,也可以自定義一個類加載器去完成,(重寫一個加載的loadClass方法)。也就是說我們可以自己解析各種類型的文件或者其他數(shù)據(jù)來獲取一個類。
類加載器這部分將在后面的篇章講解。

而對于數(shù)組類(指的是[LPackageName.YourClass,而不是數(shù)組中的元素),則有所不同,因為數(shù)組類本身不通過類加載器構建,他是由虛擬機直接創(chuàng)建。
PS:數(shù)組的元素最終還是會通過類加載器加載。

加載階段完成之后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中。然后在內存中實例化一個java.lang.Class類的對象(在HotSpot虛擬機中,這個對象存放在方法區(qū)),作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口。

加載階段與連接階段的部分內容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進行的動作,仍然屬于連接階段的內容,這兩個階段的開始時間仍然保持著固定的先后順序。

驗證

驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。

我們前面提到,加載的時候,可以構建自定義的類加載器,從指定的數(shù)據(jù)源中獲取一個類,那么就存在一定的問題,我們完全可以無視Java語言層的語法,自己生成class文件,實現(xiàn)Java語法不允許的操作,很可能會因為載入了有害的字節(jié)流而導致系統(tǒng)崩潰,所以驗證是虛擬機對自身保護的一項重要工作。

驗證階段十分重要,需要檢驗的東西也很多,但從整體上看,驗證階段大致上會完成下面4個階段的檢驗動作:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證、符號引用驗證。

  1. 文件格式驗證
    這一階段要驗證字節(jié)流是否符合Class文件格式的規(guī)范,主要基于二進制文件流進行,該階段的主要目的是保證輸入的字節(jié)流能正確的解析并存儲于方法區(qū)內。只有過了這個階段的檢驗,字節(jié)流才會進行內存的方法區(qū)中進行存儲。而后的三個檢驗都是基于方法區(qū)的存儲結構進行的,不會再操作字節(jié)流。
  2. 元數(shù)據(jù)驗證
    第二階段是對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求。的主要目的是對類的元數(shù)據(jù)信息進行語義校驗,保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息。
  3. 字節(jié)碼驗證
    第三階段是整個驗證過程中最復雜的一個階段,主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件,譬如在操作棧放置了一個int類型的數(shù)據(jù),使用時卻按long類型來加載入本地變量表中。這顯然是危險的。
  4. 符號引用驗證
    最后一個階段的校驗發(fā)生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發(fā)生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗,確保了解析動作能夠正常執(zhí)行,否則將有可能出現(xiàn)根據(jù)符號引用無法找到對應類的情況。

準備

準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區(qū)中進行分配。
PS:這里提到的是類變量,指的是靜態(tài)變量,而普通變量并不會在這一階段賦值。而且這里提到的初始值,值得是零值,具體設置的值將在初始化階段設置。

特殊的是,如果這是個靜態(tài)常量,那么就不是直接賦予零值,而是直接設置具體的值。

解析

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

  • 符號引用指的是以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
  • 直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄

前面已經(jīng)提到,解析階段可以先執(zhí)行也可以慢執(zhí)行,所以虛擬機實現(xiàn)可
以根據(jù)需要來判斷到底是在類被加載器加載時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用前才去解析它。

對于一個符號引用進行多次解析是很正常的事情,所以虛擬機會對第一次的解析結果進行緩存,如果一個符號引用之前已經(jīng)被成功解析過,那么后續(xù)的引用解析請求就應當一直成功;同樣的,如果第一次解析失敗了,那么其他指令對這個符號的解析請求也應該收到相同的異常。

解析動作主要針對接口字段類方法接口方法方法類型方法句柄調用點限定符7類符號引用進行,分別對應于常量池的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_infoCONSTANT_MethodType_infoCONSTANT_MethodHandle_infoCONSTANT_InvokeDynamic_info 7種常量類型。

初始化

初始化時類加載的最后一步,前面的類加載過程中,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼)。

在準備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始值(零值),而在初始化階段,則根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源。

從代碼的角度來說,初始化階段是執(zhí)行類構造器<clinit>()方法的過程。(不同構造函數(shù)<init>()

虛擬機會保證,在子類的<clinit>()方法執(zhí)行之前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢。因此在虛擬機中第一個被執(zhí)行的<clinit>()方法的類肯定是java.lang.Object。特殊的是,接口并不需要先執(zhí)行父接口的<clinit>(),而是等到父接口被使用時才初始化。

<clinit>()方法對于類或接口來說并不是必需的,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。

小結

簡單了解一下類加載的流程,加載、驗證、準備、解析、初始化、使用和卸載。現(xiàn)在是不是對于一個如何載入虛擬機更加清晰?
由于類加載部分在之前已經(jīng)做過筆記,這里不在重復施工。

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

推薦閱讀更多精彩內容