深刻理解Java虛擬機 - 類加載機制

代碼編譯的結果從本地機器碼轉換爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步。java

概述

虛擬機把描述一個類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機使用的Java類型,這就是虛擬機的類加載機制。程序員

類加載的時機

類加載的整個生命週期包括:加載、驗證、準備、解析、初始化、使用、卸載7個過程,其中驗證、準備和解析統稱爲鏈接。
虛擬機沒有對何時進行類的加載有強制約束,可是對於初始化階段,虛擬機規範則是嚴格規定了有且只有5中狀況必須當即對類進行初始化(加載、驗證、準備和初始化天然得在初始化前完成):編程

  1. 遇到new、getstatic、putstatic和invokestatic這四條字節碼指令時,若是類沒有進行過初始化,則須要觸發其初始化(初始化天然存在類的加載)。這四條指令最多見的場景:使用new關鍵字實例化對象、獲取或設置一個類的靜態字段(被final修飾的除外)的時候和使用一個類的靜態方法時。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是沒有對類進行過初始化,則觸發初始化。
  3. 當初始化一個子類時,發現其父類沒有初始化時,需先觸發父類的初始化。
  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類(含有main方法的類)時,虛擬機會先初始化這個類。
  5. 當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。

類加載的過程

加載

類加載階段,虛擬機須要完成如下3件事件:數組

  1. 經過一個類的全限定名獲取定義該類的二進制字節流
  2. 將字節流表明的靜態存儲結構轉換爲方法區的運行時數據結構
  3. 在方法區中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口

一個非數組類的加載,既可使用虛擬機提供的引導類加載器來完成,也能夠自定義類加載器完成(即重寫一個類加載器的loadClass方法)。對於數組類而言,狀況有所不同,數組類自己不經過類加載器建立,而是由虛擬機本身建立。bash

驗證

驗證是鏈接階段的第一步,目的是保證Class文件的字節流包含的信息符合當前虛擬機的要求,保證輸入的字節流能正確被解析並存儲於方法區。驗證階段主要包括如下4個階段:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。數據結構

  • 文件格式驗證
    第一階段驗證字節流是否符合Class文件的格式規範,而且能當前被虛擬機處理。這一階段主要驗證點:
  1. 是否以魔數開頭
  2. 版本是否在本機虛擬機處理範圍內
  3. 指向常量的各類索引值是否有指向不存在的常量
  4. ......
  • 元數據驗證
    第二階段主要是對類的元數據信息進行語義分析,保證不存在不符合Java語言規範的元數據。驗證點有:
  1. 這個類是否有父類
  2. 這個類是否繼承了不容許被繼承的類(被final修飾的類)
  3. 若是這個類不是抽象類,是否實現了其父類或繼承的接口要求實現的方法
  4. ......
  • 字節碼驗證
    第三個階段是驗證過程當中最複雜的階段,主要目的是經過數據流和控制流分析程序語義是否合法。這個階段對類的方法體進行校驗,保證被校驗方法運行時不會危害虛擬機:
  1. 保證跳轉指令不會跳轉到方法體之外的字節碼指令
  2. 保證方法體中的類型轉換時是正確的
  3. ......
  • 符號引用驗證
    最後一個階段發生在虛擬機將符號引用轉換爲直接引用的時候,這個轉換動做發生在鏈接的第三個階段 - 解析。符號引用能夠看作是對常量池中各類符號引用進行校驗,驗證點有:
  1. 符號引用中經過字符串描述的全限定名是否找到對應的類或接口
  2. 符號引用中的類、字段、方法的訪問性是否可被當前類訪問
  3. ......

準備

準備階段是正式爲類變量(被static修飾的變量,不包括實例變量)分配內存並設置類變量初始值的階段,這些內存在方法區進行分配。還有,這裏所說的初始值一般狀況下是指數據類型的零值多線程

public static int value = 123
複製代碼

value變量在準備階段後的初始值爲0,而不是123,由於這個時候並未開始執行任何java方法,而把value賦值爲123的putstatic指令時程序被編譯後,存放於類構造器方法中的,因此把value賦值爲123的操做是在初始化階段才執行的。
上面提到,一般狀況下是數據類型的零值,可是有一些特殊狀況就不同:若是類變量被final修飾,在準備階段 ,類變量就會被初始化爲指定的值編程語言

public static final int value = 123;
複製代碼

在準備階段,value的值就會被賦值爲123.函數

解析

解析階段就是虛擬機將常量池內的符號引用替換爲直接引用的過程。虛擬機規範並未對何時進行解析階段有規定,只要求了**在執行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokeestatic、involevirtual、ldc、ldc_w、multianewarray、new、putstatic和putfield這16個用於操做符號引用的字節碼指令以前,先對他們所使用的符號引用進行解析 **。因此虛擬機能夠根據須要來判斷是在類被加載器加載時就對符號引用進行解析或是在符號引用在被使用前纔去解析。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行解析。spa

  • 類或接口的解析
    假設當前代碼所處的類爲D,若是想把一個從未解析過的符號引用N解析到一個類或接口C的直接引用,虛擬機完成整個解析階段的過程分爲如下3步:
  1. 若是C是否是一個數組類型,虛擬機將會把符號引用N的全項定類名傳遞給D的類加載器去加載這個類C。在加載的過程當中因爲須要驗證,可能又會觸發其餘類的加載,一當加載過程出現錯誤,解析過程直接失敗。
  2. 若是C是一個數組類型,數組元素也是對象類型的話,N的描述符將會是相似[Ljava/lang/Integer的形式。那將會按照第一點的規則加載數組元素類型,接着由虛擬機生成一個表明此數組維度和元素的數組對象
  3. 若是前面的步驟都沒有出現錯誤,在解析完成前還須要進行符號引用的驗證,確認D是否具有對C的訪問權限,若是D沒有對C的訪問權限,拋出java.lang.IllegalAccessEroor異常。
  • 字段解析
    要解析一個未被解析過的字段的符號引用。首先會對字段表內的class_index項索引的CONSTANT_Class_info符號引用解析,也就是字段所屬的類或接口的符號引用。若是在解析這個類或接口的符號引用出現異常,都會致使字段解析的失敗。若是這個類或接口解析成功,將對這個字段所屬的類或接口用C表示,而後對C進行後續的字段搜索:
  1. 若是C自己就包含了簡單名稱和字段描述符都與目標字段相同的字段,則返回這個字段的 直接引用,查找結束
  2. 不然,若是在C中實現了接口,將會按照繼承關係從下往上遞歸搜索每一個接口和它的父接口,而後按照步驟1去查找
  3. 不然,若是C不是object類的話,按照繼承關係從下往上遞歸搜索其父類,而後按照步驟1去查找
  4. 不然,查找失敗,拋出java.lang.NoSuchFieldError異常。
  • 類方法解析
    類方法的解析的第一個步驟與字段解析同樣,也須要先解析出類方法表的claaa_index索引的方法所屬類或接口的符號引用,若是解析成功,用C表示這個類,接下來虛擬機按照如下步驟進行類方法的搜索:
  1. 在類C中查找是否有簡單名稱和描述符都與目標匹配的方法,若是有返回這個方法的直接引用,查找結束
  2. 不然在類C的父類中遞歸查找
  3. 不然在類C的接口或父接口中查找
  4. 不然查找失敗,拋出java.lang.NoSuchMethodError異常。
  • 接口方法解析
    接口方法解析與類方法解析類方法解析相似,這裏再也不冗餘。

初始化

初始化階段是類加載過程的最後一步。在前面的類加載過程當中,除了在加載階段能夠自動定義加載器參與類的加載過程外,其他的動做徹底由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java代碼
在準備階段,變量已經被賦值爲系統要求的零值,而在初始化階段,則根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源。或者說初始化階段是執行類構造器方法的過程。

  1. 方法是由編譯器自動收集類中的全部類變量的複製操做和靜態語句塊(static{})中的全部語句合併而生的。靜態語句塊只能訪問到定義在靜態語句前的變量,定義在它以後的變量,只能在靜態語句塊中賦值而不能訪問。
  2. 方法與類的構造函數不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類的方法執行前執行父類的方法。
  3. 方法並非必須的,若是一個類沒有靜態語句塊,也沒有變量的賦值操做,編譯器能夠不爲這個類生成方法。
  4. 虛擬機會保證一個類的方法在多線程環境下被正確地加鎖、同步,若是多線程同時去初始化一個類,只會有一個線程執行方法。

類與類加載器

虛擬機設計團隊把類加載階段的經過一個類的全限定名獲取此類的二進制字節流這個動做放到Java虛擬機外部實現,以便讓開發人員本身決定如何獲取所須要的類,實現這個動做的代碼模塊稱爲「類加載器」。
對於任意一個類,都須要加載它的類加載器和這個類自己一同肯定其所在虛擬機的惟一性。通俗地說,比較兩個類是否相等,只有在相同的類加載器的前提下才有意義,不然,即便這兩個類來自於同一個Class文件,被同一個虛擬機加載,只要類加載器不同,這兩個類就不可能相等。

雙親委派模型

從虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器,是虛擬機的一部分;另外一種是其餘的類加載器,獨立於虛擬機以外,並且全都繼承於抽象類java.lang.ClassLoader.
從開發人員的角度來看,絕大部分java程序都會使用到如下3種系統提供的類加載器:

  • 啓動類加載器
    這個類負責將放在<JAVA_HOME>\lib目錄下的而且被虛擬機識別的(按照文件名識別,名字不符合的類庫即便放在lib目錄下也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被java程序直接引用。

  • 拓展類加載器
    它負責加載<JAVA_HOME>\lib\ext目錄下的全部類庫,開發者能夠直接使用拓展類加載器

  • 引用程序類加載器
    它負責加載用戶類路徑(ClassPath)下所指定的類庫,開發者能夠直接使用。若是程序中沒有自定義本身的類加載器,通常狀況下這個就是程序默認的類加載器。

雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應該有本身的父類加載器、可是這裏的類加載器之間的父子關係不是以繼承關係實現的,而是使用組合關係來複用父類加載器。

  • 雙親委派模型的工做流程: 若是一個類加載器收到了一個類加載請求,它不會本身去加載這個類,而是將請求委派給它的父類加載器去加載,每個層次的類加載器都是這樣,所以全部的類加載請求最終都會落到頂層的啓動類加載器,只有當父類加載器五法加載這個請求時(它的搜索範圍中沒有找到所需的類),子加載器纔會嘗試本身去加載。使用雙親委派的好處就是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。
相關文章
相關標籤/搜索