1. 程式人生 > >虛擬機類加載機制詳解

虛擬機類加載機制詳解

cnblogs lpad 返回值 虛擬機啟動 rec 關鍵字 ted 抽象類 運行

目錄:

  1.類加載的時機

  2.類加載的過程

  3.類加載器

一、類加載的時機

  類從被加載到虛擬機內存中開始,到卸載除內存為止,他的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading),這七個階段的發生順序如下圖

  技術分享

  上圖中,加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須要按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支持Java語言的動態綁定。這些階段通常是互相交叉地混合式進行的,通常會在一個階段執行的過程中調用、激活另外一個階段。

  對於加載而言,Java虛擬機規範中並沒有進行強制約束,這一點可以交給虛擬機的具體實現來自由把握

  而對於初始化階段,虛擬機規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要再此之前開始):

    1)遇到了new、getstatic、putstatic或invokestatic這4個字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。

    2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

    3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

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

    5)當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

二、類加載的過程

  1.加載:

    “加載”是“類加載”(Class loading)過程的一個階段,希望大家不要混淆兩個名詞。在加載階段,虛擬機需要完成以下三件事情:

    1)通過一個類的全限定名來獲取定義此類的二進制字節流

    2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構

    3)在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口

    相較於類加載過程的其他階段,一個非數組類的加載階段(準確的說,是加載階段中獲取類的二進制字節流的動作)是開發人員可控性最強的,因為加載階段既可以使用系統提供的引導類加載器來完成,也可以由用戶自定義的累加器去完成,開發人員也可以通過定義自己的類加載器去控制字節流的獲取方式(即重寫一個類加載氣的loadClass()方法)

  2.驗證:

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

    驗證是虛擬機對自身保護的一項重要工作,驗證階段非常重要,這個階段是否嚴謹,直接決定了Java虛擬機是否能承受惡意代碼的攻擊,從執行性能的角度上講,驗證階段的工作量在虛擬機的類加載子系統中又占了相當大的一部分,從整體上看,驗證階段大致上會完成下面4個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證

    1)文件格式驗證:第一階段要驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理,該驗證階段的主要目的是保證輸入的字節流能正確地解析並存儲於方法區之內,格式上符合描述一個Java類型信息的要求。這階段的驗證是基於二進制字節流進行的,只有通過了這個階段的驗證後,字節流才會進入內存的方法區中進行存儲,所以後面的3個驗證階段全部都是基於方法區的存儲結構進行的,不會再直接操作字節流

    2)元數據驗證:第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,這個階段可能包含的驗證點有:

      這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)

      這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)

      如果這個類不是抽象類,是否實現了其父類或接口之中要求實現的所有方法

      類中的字段、方法是否與父類產生矛盾(例如,覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不同等)

      ....

      第二階段的主要目的是對類的元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息

    3)字節碼驗證:第三階段是整個驗證過程中最復雜的一個階段,主要目的是通過數據流和控制流分析確定程序語義是合法的、符合邏輯的。在第二階段是對元數據信息中的數據類型做完校驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件

      如果一個類方法體的字節碼還沒有通過字節碼驗證,那肯定是有問題的;但是如果一個方法體通過了字節碼驗證,也不能說明其一定就是安全的。

    4)符號引用驗證:最後一個階段的教研發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三階段-—解析階段中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗,通常需要校驗下列內容:

      符號引用中通過字符串描述的全限定名是否能找到對應的類

      在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段

      符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可以被當前類訪問

      ....

      符號引用驗證的目的是確保解析動作能正常執行,如果無法通過符號引用驗證,將拋出一個異常

      對於虛擬機的類加載機制來說,驗證階段是一個非常重要的、但不是一定必要(因為對程序運行期沒有影響)的階段。

  3.準備

    準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中分配。這個階段中有兩個容易產生混淆的概念需要強調一下,首先,這時候進行內存分配的盡包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在java堆中。其次,這裏所說的初始值“通常情況下”是數據類型的零值

    基本數據類型的零值:

數據類型 零值 數據類型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char ‘\u0000’ reference null
byte (byte)0

  4.解析

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

    1.符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標不一定已經加載到內存中。各種虛擬機實現的內存布局可以各不相同,但是他們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中

    2.直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個鞥你間接定位到目標的句柄。直接引用可以是和虛擬機實現的內存布局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在

  5.初始化

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

    在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度去表達:初始化階段是執行類構造器<clinit>()方法的過程

三、類加載器

  虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊叫做“類加載器”

  1.類與類加載器:

    類加載器雖然只用於實現類的加載動作,但它在Java程序中起到的作用卻遠遠不限於類加載階段。對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類命名空間。這句話可以表達的更加通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等

  2.雙親委派模型

    從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用c++實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全都集成自抽象類java.lang.ClassLoader

    從Java開發人員角度來看,類加載器還可以分得再細致一些,絕大部分Java程序都會使用到以下三種系統提供的類加載器

    1)啟動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在<JAVA_HOME>\lib目錄中,或者被-Xbootclasspath參數所制定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會加載)類庫加載到虛擬機內存中。啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,需要把加載請求為派給引導類加載器,那直接使用null代替即可

    2)擴展類加載器(Extension ClassLoader):這個加載器由sun.misc,Launcher$ExtClassloader實現,它負責加載<JAVA_HOME>\lib\ext,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器

    3)應用程序加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$AppClassLoader實現。由於這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器

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

     技術分享

    上圖展示的類加載器之間的這種層次關系,稱為類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。這裏類加載器之間的父子關系一般不會以繼承(Inheritance)的關系的實現,而是都使用組合(Composition)關系來復用父加載器的代碼

補充:

  參考:java虛擬機原理

  本次博客是對java虛擬機原理的虛擬機類加載機制章節的總結與整理,想要深入學習本章詳細知識點的可以去看此書的本章節

虛擬機類加載機制詳解