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()方法)
數組類不經過類加載器建立,由虛擬機直接建立。spa

驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保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)。
相關文章
相關標籤/搜索