虛擬機類加載機制(五)--- 類加載的過程

Java的技術體系包括

  • 支持Java程序運行的虛擬機(JVM)
  • 提供接口支持的Java API
  • Java 編程語言
  • 第三方Java框架(如Spring等)

代碼編譯的結果從本地機器碼轉變?yōu)樽止?jié)碼澜沟,是存儲格式發(fā)展的一小步,確實編程語言的一大步。


前面幾篇文章詳細介紹了java代碼編譯后形成的Class文件的結構烁挟,編譯成Class文件之后java虛擬機才能夠識別相應的代碼程序怕犁。那么虛擬機會如何把Class文件加載到內(nèi)存呢?并會對加載的Class文件做哪些處理呢?這就是類加載的過程猾警。

虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存炬太,并對數(shù)據(jù)進行校驗灸蟆、轉換解析和初始化,最終形成能被虛擬機直接使用的java類型的過程亲族,就是類加載機制炒考。

java語言最大的特點之一就是不需要在編譯期間進行類的加載和連接可缚,而是在程序運行期間去完成。這種策略雖然會使類加載的時候增加一些性能開銷斋枢,但是卻為java語言提供了高度的靈活性帘靡,主要體現(xiàn)在

  1. 接口可以在運行時再指定實際實現(xiàn)的類
  2. 通過自定義類加載器,可以讓程序在運行時從網(wǎng)絡加載一個二進制流作為程序代碼的一部分杏慰。而這種方式目前已經(jīng)被廣泛運用到java語言應用的各個領域测柠,從最初的Applet,JSP到OSGi缘滥,再到Android的熱更新等等轰胁。

類的整個生命周期

類從被加載到虛擬機內(nèi)存開始,到被卸載出內(nèi)存為止朝扼,它的整個生命周期包括:加載赃阀、驗證、準備擎颖、解析榛斯、初始化、使用搂捧、卸載7個階段驮俗,可以用下圖來表示

其中,類的加載允跑,驗證王凑,準備,初始化聋丝,卸載這五個階段的順序是確定的索烹,類的加載過程會按照這個順序開始,但彼此并沒有嚴格的界限弱睦,而是通常在一個階段執(zhí)行的過程中會激活另一個階段百姓。解析階段則有時候可以出現(xiàn)在初始化之后,這是為了支持java語言的運行時綁定况木。

類加載的時機

什么時候會開始類加載的第一個階段垒拢,加載呢?java虛擬機規(guī)范中沒有嚴格的要求火惊。但是對類初始化階段做了嚴格的規(guī)定子库,有且只有下面5中情況時,需要立即對類進行初始化

  1. 遇到 new, getstatic, putstatic, invokestatic 這四個字節(jié)碼指令時矗晃。對應的java代碼場景是:實例化一個對象,讀取或者設置一個類的靜態(tài)變量(被final修飾的靜態(tài)常量除外宴倍,final static 會在編譯期被放入常量池)张症,調用一個類的靜態(tài)方法仓技。
  2. 使用java.lang.reflect包的方法對類進行反射調用時。
  3. 當初始化一個類的時候俗他,如果發(fā)現(xiàn)其父類沒有初始化脖捻,需要先觸發(fā)其父類的初始化。
  4. 當虛擬機啟動時兆衅,用戶需要指定一個要執(zhí)行的主類(包含main()方法的哪個類)地沮,虛擬機會先初始化這個主類。
  5. 如果一個 java.lang.invoke.MethodHandle 實例最后的解析結果為 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄時羡亩。
    除了上面5中情況之外摩疑,其他任何場景都不會對類進行初始化。而在類進行初始化之前畏铆,必然要先進行加載雷袋,驗證,準備辞居。

注意接口與類的區(qū)別在于第3條楷怒,當一個類在進行初始化時,要求其父類全部都已經(jīng)初始化過瓦灶,但在一個接口初始化時鸠删,并不要求其父接口初始化,只有在真正使用到父接口的時候贼陶,比如引用接口中定義的常量刃泡,才會初始化父接口。

類加載的過程

加載

虛擬機使用類需要進行的第一個動作就是加載每界,虛擬機需要做3件事

  1. 通過類的全限定名獲取類的二進制字節(jié)流
  2. 將二進制流所代表的靜態(tài)數(shù)據(jù)結構(也就是Class文件結構)捅僵,轉換為內(nèi)存中方法區(qū)運行時的數(shù)據(jù)結構
  3. 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,用來訪問方法區(qū)該類的各種數(shù)據(jù)眨层。

以上虛擬機規(guī)范中所規(guī)定的動作庙楚,最被廣為應用的就是第一個,也就是說虛擬機規(guī)范沒有規(guī)定二進制字節(jié)流只能來源于Class文件趴樱,它可以有任意多的來源馒闷,比如用的最多jar包,Android的Dex文件叁征,已經(jīng)被更廣泛應用的從網(wǎng)絡中獲取二進制字節(jié)流纳账。只要所獲得的二進制字節(jié)流符合Class結構,就可以被認定為一個規(guī)范的類捺疼。

而當類被加載到方法區(qū)之后疏虫,已怎樣的結構存儲,虛擬機規(guī)范中并沒有規(guī)定,這個依賴于與虛擬機的具體實現(xiàn)卧秘。之后在內(nèi)存中實例化的 java.lang.Class對象呢袱,虛擬機也沒有規(guī)定在內(nèi)存的哪個區(qū)域,比如HotSpot虛擬機就將這種特殊的Class對象存儲在了方法區(qū)翅敌。

驗證

加載之后羞福,緊接著的是驗證階段,而且在虛擬機將Class二進制字節(jié)流加載到內(nèi)存的過程中蚯涮,驗證階段就要開始了治专。驗證的目的是為了確保Class文件的二進制字節(jié)流符合虛擬機規(guī)范,同時不會危害虛擬機的安全遭顶。因為Class文件的來源可以非常廣泛张峰,并不一定是由java源碼編譯而來,極端情況下甚至可以直接由十六進制編輯器編輯而來液肌,因此Class文件的二進制字節(jié)流是不可信任的挟炬,必須對齊進行驗證。

驗證階段大體可以分為4個階段的驗證工作

  1. 文件格式驗證

首先要驗證字節(jié)流符合Class文件格式規(guī)范嗦哆,格式上符合描述一個java類型信息的要求谤祖,以便字節(jié)流能被正確的解析并存儲于方法區(qū)中。通過該階段的驗證后老速,字節(jié)流就會進入內(nèi)存的方法區(qū)并存儲粥喜,后面的3個階段均基于方法區(qū)的存儲結構進行驗證。

  1. 元數(shù)據(jù)驗證

第二階段是對類的元數(shù)據(jù)進行語義校驗橘券,保證其描述的信息符合java語言規(guī)范的要求额湘。比如一個類是否有父類,是否繼承了不被允許繼承的類(被final修飾)等等

  1. 字節(jié)碼驗證

該階段是整個驗證階段最復雜的階段旁舰,是通過對數(shù)據(jù)流和控制流的分析锋华,確定程序的語義是合法的并且符合邏輯的。還記得我們之前將一個java類可以分為元數(shù)據(jù)方法體中的代碼邏輯兩部分么箭窜?第二階段就是對元數(shù)據(jù)的驗證毯焕,而第三階段就是對方法體中的代碼邏輯進行驗證,以保證不會做出危害虛擬機的事情磺樱。

JDK1.6之后纳猫,為了避免過多的時間性能消耗在字節(jié)碼驗證階段,對javac編譯器和虛擬機做了優(yōu)化竹捉,在方法體的Code屬性中增加了一項 "StackMapTable" 屬性芜辕,將原來的類型推倒驗證轉化為了類型檢查驗證,從而來節(jié)約時間块差。JDK1.7之后編譯的Class文件侵续,只能通過 "StackMapTable" 類型檢查驗證的方式來完成字節(jié)碼驗證倔丈。

  1. 符號引用驗證

該階段發(fā)生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作發(fā)生在解析階段询兴,目的是對類自身以外的信息進行匹配性校驗以確保解析動作能夠正確的執(zhí)行乃沙。比如驗證字符串描述的全限定名的類能否找到,以及類中是否存在引用的字段和方法等等诗舰。

對于虛擬機而言,驗證階段是一個非常重要训裆,但并不是必需的階段眶根。所以如果所運行的全部代碼已經(jīng)被反復的使用和驗證,那么可以考慮關閉類驗證措施边琉,以縮短類加載時間属百。

準備

準備階段是正式為類變量(也就是屬于類的變量,即類靜態(tài)變量变姨,static)分配內(nèi)存并設置初始值的階段族扰,類變量的內(nèi)存將在方法區(qū)中進行分配。這里有3點需要注意

  1. 這里分配在內(nèi)存方法區(qū)的僅僅為類變量定欧,不包括實例變量渔呵,實例變量將在對象實例化時被分配在Java堆。
  2. 這里所說的初始值指的是對應數(shù)據(jù)類型的零值砍鸠,例如
public static int value = 123;

此時在準備階段時扩氢,value的值為0。因為此時還沒有執(zhí)行任何的java方法爷辱,而把123賦值給value的 putstatic 指令是被存放在類構造器 <clinit> 方法之中录豺,這個動作在初始化階段才會執(zhí)行。

  1. 如果同時被 final static 修飾的話饭弓,例如
public final static int value = 123;

在準備階段就會把123賦值給value双饥。因為對于被 final static 修飾的變量,編譯器會為該變量生成 ConstantValue 屬性弟断,因為在準備階段咏花,虛擬機就會根據(jù) ConstantValue 屬性所指向的常量池中的值賦給value,而不需要通過 putstatic 的字節(jié)碼命令賦值夫嗓,因此準備階段就可以完成迟螺。

解析

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

關于符號引用和直接引用舍咖,兩者之間的區(qū)別如下

  1. 符號引用矩父,使用符號來描述所引用的目標,符號的形式被明確定義在Class文件格式中(比如各種字面量和UTF-8編碼字符串)排霉。符號引用的目標不一定被加載到了內(nèi)存中窍株。
  2. 直接引用,可以是直接指向目標的指針、相對偏移量球订,或者是一個能間接定位到目標的句柄后裸。如果有了直接引用,那么目標必定已經(jīng)在內(nèi)存中冒滩。

也就是說微驶,解析其實就是解析Class文件常量池中的符號引用。所以什么時候開始解析呢开睡?自然是在執(zhí)行用于操作"符號引用"的字節(jié)碼指令之前因苹。而各虛擬機可以根據(jù)需要,自己去實現(xiàn)篇恒,是在類加載之后就對常量池中的符號引用進行解析扶檐,還是在真正使用一個符號引用的時候才進行解析。

下面詳細闡述下常量池中最常用的4類符號引用的解析過程

類或者接口的解析 --- 對應常量池的 CONSTANT_Class_info 類型的解析

如果當前代碼所處的類為D胁艰,要解析一個符號引用N款筑,解析為類或者接口C的直接引用,過程如下

  1. 如果C不是一個數(shù)組類型腾么,那么虛擬機會把N代表的全限定名傳遞給D的類加載器去加載這個類C奈梳。
  2. 如果C是一個數(shù)組類型,并且數(shù)組元素類型為對象哮翘,就會按照1去加載數(shù)組元素類型颈嚼,接著由虛擬機生成一個代表此數(shù)組維度和元素的數(shù)組對象。
  3. 如果前兩個步驟順利饭寺,那么C在虛擬機中已經(jīng)成為一個有效的類或者接口阻课,接下來進行符號引用驗證,驗證D是否具備對C的訪問權限艰匙。
字段解析 --- 對應常量池的 CONSTANT_Fieldref_info 類型的解析

解析一個字段的符號引用限煞,會首先解析字段表內(nèi)的 CONSTANT_Class_info 符號引用,也就是字段所屬的類的符號引用员凝。如果解析成功署驻,將該字段所屬的類用C來表示,會對字段進行如下解析過程

  1. 如果C中本身就有相應的字段健霹,則返回這個字段的直接引用旺上,查找結束。
  2. 否則糖埋,如果C中實現(xiàn)了接口宣吱,會按照繼承關系從下到上遞歸搜索各接口和它的父接口,如果有相應的字段瞳别,則返回直接引用征候,查找結束杭攻。
  3. 否則,按照繼承關系從下往上遞歸搜索其父類疤坝,如果有相應字段兆解,則返回直接引用,查找結束跑揉。
  4. 否則锅睛,查找失敗,拋出 java.lang.NoSuchFieldError 異常历谍。
    當返回直接引用之后衣撬,會對字段進行符號引用驗證,驗證是否具備對字段的訪問權限扮饶,如果不具備,將拋出 java.lang.IllegalAccessError 異常
類方法解析 --- 對應常量池 CONSTANT_Methodref_info 類型的解析

類方法解析的第一個步驟與字段解析一樣乍构,也需要先解析出類方法表的 CONSTANT_Class_info 符號引用甜无,如果解析成功,我們依然用C來表示這個類哥遮,接下來過程如下

  1. 如果類方法表中發(fā)現(xiàn) 類的符號引用 是一個接口岂丘,則拋出異常 java.lang.IncompatibleClassChangeError
  2. 如果通過1,在類C中查找是否有相應方法眠饮,有則返回直接引用奥帘,查找結束
  3. 否則,在類C的父類中遞歸查找是否有相應方法仪召,有則返回直接引用寨蹋,查找結束
  4. 否則,在類C實現(xiàn)的接口列表以及他們的父接口中查找是否有相應的方法扔茅,如果有已旧,說明C是一個抽象類,查找結束召娜,拋出異常 java.lang.AbstractMethodError 異常
  5. 否則运褪,查找失敗,拋出 java.lang.NoSuchMethodError
    如果返回了直接引用玖瘸,會對該方法進行符號引用驗證秸讹,驗證是否具備對方法的訪問權限
接口方法解析 --- 對應常量池 CONSTANT_InterfaceMethodref_info 類型解析

接口方法解析也需要先解析出接口方法表的 CONSTANT_Class_info符號引用,接下來過程如下

  1. 如果接口方法表中發(fā)現(xiàn) 接口的符號引用 是一個類雅倒,則拋出異常 java.lang.IncompatibleClassChangeError
  2. 否則璃诀,在接口C中查找是否有相應的方法,有則返回直接引用屯断,查找結束
  3. 否則文虏,在接口C的父接口中遞歸查找是否有相應的防范侣诺,有則返回直接引用,查找結束
  4. 否則氧秘,查找失敗年鸳,拋出 java.lang.NoSuchMethodError
    接口中的方法默認都是public,不存在訪問權限的問題丸相。

初始化

初始化階段是類加載的最后一個階段搔确。該階段主要是執(zhí)行類構造器 <clinit> 的過程。在前面的整個過程中灭忠,只有在加載階段膳算,用戶可以通過自定義類加載器參與,其他所有過程都是虛擬機自動完成的弛作。到了初始化階段涕蜂,才真正開始執(zhí)行類中定義的java程序代碼。

關于類構造器 <clinit> 有以下幾點說明

  1. <clinit> 方法是由編譯器根據(jù)類靜態(tài)變量和靜態(tài)語句塊合并產(chǎn)生映琳,并按照在java源文件中的順序進行合并机隙。
  2. 虛擬機會在執(zhí)行 <clinit> 方法之前,先執(zhí)行其父類的 <clinit> 方法萨西。因此有鹿,虛擬機執(zhí)行的第一個 <clinit> 方法,一定是 java.lang.Object 的谎脯。
  3. 父類的靜態(tài)語句塊要優(yōu)先與子類的靜態(tài)變量賦值操作
  4. <clinit> 并不是必須的葱跋,編譯器可以不給一個類生成該方法
  5. 接口的 <clinit> 方法。不需要先執(zhí)行接口父類的 <clinit> 方法源梭,只有需要使用父接口定義的靜態(tài)變量娱俺,才會調用父接口的 <clinit> 。同時咸产,接口的實現(xiàn)類也一樣不會執(zhí)行接口的 <clinit> 方法矢否。
  6. 在多線程環(huán)境下,虛擬機會自動保證 <clinit> 方法的加鎖和同步脑溢。即僵朗,如果多個線程同時去初始化一個類,那么只有一個線程去執(zhí)行這個類的 <clinit> 方法屑彻,其他線程都需要阻塞等待验庙,知道活動線程執(zhí)行完 <clinit> 方法。因此如果在 <clinit> 中執(zhí)行耗時很長的操作社牲,就會造成多進程阻塞粪薛。這種阻塞往往又是非常隱蔽的。
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末搏恤,一起剝皮案震驚了整個濱河市违寿,隨后出現(xiàn)的幾起案子湃交,更是在濱河造成了極大的恐慌,老刑警劉巖藤巢,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巾乳,死亡現(xiàn)場離奇詭異葬项,居然都是意外死亡啸如,警方通過查閱死者的電腦和手機缘挑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绍刮,“玉大人温圆,你說我怎么就攤上這事『⒏铮” “怎么了岁歉?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長膝蜈。 經(jīng)常有香客問我刨裆,道長,這世上最難降的妖魔是什么彬檀? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮瞬女,結果婚禮上窍帝,老公的妹妹穿的比我還像新娘。我一直安慰自己诽偷,他們只是感情好坤学,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著报慕,像睡著了一般深浮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上眠冈,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天飞苇,我揣著相機與錄音,去河邊找鬼蜗顽。 笑死布卡,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的雇盖。 我是一名探鬼主播忿等,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼崔挖!你這毒婦竟也來了贸街?” 一聲冷哼從身側響起庵寞,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎薛匪,沒想到半個月后捐川,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡蛋辈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年属拾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冷溶。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡渐白,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出逞频,到底是詐尸還是另有隱情纯衍,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布苗胀,位于F島的核電站襟诸,受9級特大地震影響,放射性物質發(fā)生泄漏基协。R本人自食惡果不足惜歌亲,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望澜驮。 院中可真熱鬧陷揪,春花似錦、人聲如沸杂穷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽耐量。三九已至飞蚓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間廊蜒,已是汗流浹背趴拧。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留山叮,地道東北人八堡。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像聘芜,于是被迫代替她去往敵國和親兄渺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

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