Java虛擬機-類加載機制

概述

虛擬機把描述類的數據從Class文件加載到內存,並且對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
編譯時無需進行連接工作,類的加載、連接和初始化過程都是在程序運行期間完成的。如面向接口的應用程序可以等到運行時再指定其實際的實現類;用戶可以通過預定義或者自定義的類加載器,讓本地的應用程序可以在運行時從網絡或者其他地方加載一個二進制流作爲程序代碼的一部分。

類加載的時機

類從被加載到虛擬機內存中開始,到卸載出內存爲止,生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。

加載

5種情況必須立即對類進行初始化:

  1. 使用new實例化對象、讀取或者設置一個類的靜態字段(被final修飾在編譯期以及把結果放入常量池的靜態字段除外)、調用一個類的靜態方法。
  2. 使用java.lang.reflect包的方法對類進行反射調用時,對應的類沒有經過初始化,則需要觸發其初始化。
  3. 初始化一個類時如果其父類沒有初始化,則需要先觸發父類的初始化。
  4. 虛擬機啓動時,用戶指定類一個要執行的主類,虛擬機會先初始化這個類。
  5. JDK1.7如果一個java.lang.invoke.MethodHandle實例最後解析結果位REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有經過初始化,則需要觸發其初始化。
    除了上面這5種之外,其他所有引用的方式都不會觸發初始化,稱爲被動引用。

類加載的過程

過程主要包括加載、驗證、準備、解析、初始化這5個階段

加載

加載階段,虛擬機主要完成下面3件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節所代表的靜態存儲結構轉化爲方法區運行時的數據結構。
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。

通過類的全限定名獲取類的二進制字節流有多種方式,如:

  1. 從Zip、Jar、Ear、War等包種讀取。
  2. 從網絡種獲取,Applet。
  3. 運行時計算生成,動態代理技術。
  4. 其他文件生成,如JSP
    等等

非數組類可以使用系統提供的引導類加載器完成,也可以用戶自定義類加載器(重寫類加載器的loadClass()方法)
數組類不通過類加載器創建,由虛擬機直接創建。

驗證

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

  1. 文件格式驗證(確保類文件結構符合JVM標準)
  2. 元數據驗證(驗證元數據信息種的數據類型,信息符合Java規範,是否有父類,是否實現接口中的方法,重載是否符合規則等等)
  3. 字節碼驗證(分析數據流和控制流確定程序語義是合法的,對方法體進行校驗分析,例如類型安全,指令不能隨意跳轉)
    Halting Problem(停機問題)用程序去校驗邏輯是無法做到絕對準確的。
  4. 符號引用驗證(類、字段、方法是否能夠訪問)

準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區種進行分配。

解析

解析階段是虛擬機將常量池內的符號引用替換位直接引用的過程,符號引用在Class文件種以CONSTSNT_Class_info、CONSTSNT_Fieldref_info、CONSTSNT_Methodref_info等類型的常量出現。
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。
直接引用(Direct References):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同,如果有直接引用,那引用的目標必定已經在內存中存在。

  1. 類或接口的解析
    假設類爲D,要把一個從未解析過的符號引用N解析爲一個類或者接口C的直接引用,解析過程分爲三步:
    1)C不是數組類型,虛擬機把N的全限定名傳遞給D的類加載器去加載類C,過程中一旦出現異常,解析宣告失敗。
    2)C是數組類型且元素類型是對象,按照1的規則加載數組元素類型,接着由虛擬機生成一個代表此數組維度和元素的數組對象。
    3)上面兩步沒有任何異常,C在虛擬機中已經成爲一個有效的類或者接口,但是解析完成前還要進行符號引用驗證,確認D是否具備對於C的訪問權限。

  2. 字段解析
    解析字段符號引用,首先對字段表內class_index中索引的CONSTANT_Class_info符號引用進行解析,就是類或者接口符號引用,如果過程中發生異常會導致解析失敗,如果成功將這個字段所屬的類或者接口以C表示,後續操作爲:
    1)C本身包含簡單名稱和字段描述符都與目標匹配的字段,返回這個字段直接引用查找結束
    2)如果C實現了接口,會按照繼承關係從下往上查找,如果找到直接引用查找結束
    3)如果C不是java.lang.Object,按照繼承關係從下往上查找,如果找到直接引用查找結束
    4)都沒找到,拋出java.lang.NoSuchFieldError。如果找到還要判斷是否具有訪問權限

  3. 類方法解析
    和字段解析相似,我們假設方法所屬的類解析成功爲C,後續操作:
    1)如果在類方法表中發現class_index中索引的C是個接口,就拋出java.lang.IncompatibleClassChangeError,也就是類的方法不能是一個接口方法或者抽象方法,是必須實現的。
    2)如果第一步沒問題,就在類C中查找是否有簡單名稱和描述符都與目標匹配的方法,如果找到直接引用查找結束
    3)否則在C的父類中遞歸查找,如果找到直接引用查找結束
    4)否則在C的接口列表和父接口中遞歸查詢,如果找到說明C是一個抽象類,查找結束拋出java.lang.AbstractMethodError異常。
    5)都沒有找到,拋出java.lang.NoSuchMethodError,如果找到還要判斷是否具有訪問權限

  4. 接口方法解析
    和類方法相似,設定方法所屬接口解析成功爲C,後續操作:
    1)如果發現class_index的索引C是一個類而不是接口,就拋出java.lang.IncompatibleClassChangeError
    2)在接口C中直接查找,如果找到直接引用查找結束
    3)在接口C的父接口中遞歸查找,直到java.lang.Object類,如果找到直接引用查找結束
    4)都沒有找到,拋出java.lang.NoSuchMethodError,接口的方法默認都是public,因此不具有訪問權限問題

初始化

類初始化是類加載過程的最後一步,這裏開始執行類中定義的Java程序代碼(或者說字節碼)。初始化階段是執行類構造器() 方法的過程。這個方法是由編譯期自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合併而成的。
() 方法與類構造函數()方法不同,不需要顯式調用父類構造器,虛擬機會保證在子類的() 方法執行前,父類的() 已經執行完畢,第一個被執行的() 方法一定是java.lang.Object。

類加載器

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

類與類加載器

任意一個類都要由加載他的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器都擁有一個獨立的類名稱空間。兩個類是否相等,需要來源於同一個Class文件,被同一個虛擬機加載,同一個類加載器。

雙親委派模型

Java虛擬機只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另一種就是其他所有的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全部都繼承自抽象類java.lang.ClassLoader。
根據更細緻的分法,絕大部分Java程序都會使用到以下3種系統提供的類加載器。

  1. 啓動類加載器(Bootstrap ClassLoader)
    這個類加載器負責將存放在\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別如rt.jar,名字不符合的即使放在路徑中也不會被加載),啓動類加載器無法被Java程序直接引用。
  2. 擴展類加載器(Extension ClassLoader)
    這個類加載器由sun.misc.Launcher$ExtClassLoader實現,負責加載\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
  3. 應用程序類加載器(Application ClassLoader)
    這個類加載器由sun.misc.Launcher$AppClassLoader實現,這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱之爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般就是使用這個類加載器。

類加載器的雙親委派模式(Parents Delegation Model)是指除了頂層的啓動類加載器外,其餘的類加載器都應當由自己的父類加載器,類加載器之間的父子關係一般不會以繼承(Inheritantce)的關係來實現,而是都使用組合(Composition)的關係來複用父加載器的代碼。
雙親委派模型的工作過程是:如果一個類加載器收到類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,所有的加載請求最終都傳送到頂層的啓動類加載器中,只有父加載器無法完成加載請求(它的搜索範圍中沒有找到所需要的類),子加載器纔會嘗試自己去加載。
這樣Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係,例如java.lang.Object,它在rt.jar中,無論哪個類加載器要加載這個類最終都是交給頂層啓動類加載器進行加載,這樣Object類在所有的加載器環境中都是同一個類。

破壞雙親委派模型

  1. 由於JDK1.0時代就存在類加載器,而雙親委派模型在JDK1.2之後才被引入,所以爲了兼容做了一些調整。
  2. 例如JNDI的場景中,JNDI的代碼有啓動類加載器加載,但是要調用在ClassPath下的接口提供者的代碼。所以Java引入的線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader() 方法進行設置,如果創建線程時未設置會從父線程中繼承一個,如果應用全局都沒有設置過,那這個類加載器默認就是應用程序加載器。JDNI會使用這個類加載器去加載服務商代碼,所以實際是父類加載器請求子類加載器去完成類加載動作。
  3. 程序的動態性。代碼熱替換(HotSwap),模塊熱部署(HotDeployment)。JSR-291(OSGi R4.2)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章