天天看點

虛拟機類加載機制

虛拟機類加載機制:虛拟機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的

Java類型。

類的加載、連接配接、初始化都是在程式運作期間完成。

類的生命周期:加載(loading) --> 驗證(verification) --> 準備(preparation) --> 解析(resolution) --> 初始化(initialization)

--> 使用(using) --> 解除安裝(unloading)。其中驗證、準備、解析三個部分統稱連接配接(linking)。

遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時;java代碼場景: 使用new

執行個體化對象、讀取、設定類的靜态字段(被final修飾、已在編譯器把結果放入常量池的靜态字段除外)、調用類的靜态方法。

使用java.lang.reflect包的方法對類進行反射調用的時候。

初始化一個類時,發現其父類沒有進行過初始化,則先觸發其父類的初始化。(接口在真正使用父接口的時候才初始化)

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

使用jkd 1.7 的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getStatic、REF_putStatic、

REF_invokeStatic的方法句柄,并且這個方法句柄對于的類沒有進行初始化。

有且隻有這五種場景對主動觸發初始化,這種行為成為主動引用。

被動引用:

通過子類引用父類的靜态字段,不會導緻子類初始化。靜态字段隻有直接定義這個字段的類才會被初始化,通過其子類來引用父類中定義的

靜态字段,隻會觸發父類初始化不觸發子類初始化。

通過數組定義來引用類,不會觸發此類的初始化。

public static void main(String[] args) {

}

什麼都沒有輸出

常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,是以不會觸發定義常量類的初始化。

public class ConstClass {

輸出結果:

hello world

類加載過程包括:加載、驗證、準備、解析、初始化

虛拟機主要要完成:

通過一個類的全限定名來擷取定義此類的二進制位元組流

将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構

在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種資料通路入口

加載階段完成後,虛拟機外部的二進制位元組流就按照虛拟機所需的格式存儲在方法區中。

類加載:自定義類的加載器;系統提供引導類加載器

確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,不會危害虛拟機自身的安全

驗證位元組流是否符合Class檔案格式規範,并且能夠被目前版本虛拟機處理

是否以魔數0xCAFEBABE開頭

主、次版本号是否在目前虛拟機處理範圍内

常量池的常量中是否有不被支援額常量類型

等等

主要目的:包裝輸入的位元組流能正确的解析并存儲在方法區内,格式上符合描述一個Java類型資訊的要求。

隻有通過驗證,位元組流才會進入記憶體的方法區中進行存儲,後續的驗證階段全部是幾區方法區的存儲結構進行的。

對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合java語言規範

這個類是否有父類

這個類父類是否繼承了不允許繼承的類

這個類是不是抽象類,是否實作了父類或者接口中要求實作的所有方法

......

通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。

校驗類的方法體,保證被校驗類在運作時不會做出危害虛拟機安全的事件

對類自身以外的資訊進行比對校驗

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

指定類中是否存在符合方法的字段描述

符合引用中的類、字段、方法的通路性是否可被目前類通路

目的:確定解析動作能夠正常執行

發生在虛拟機将符号引用轉換為直接引用時

準備階段是正式為類變量(被static修飾的變量)配置設定記憶體并設定類變量初始值的階段,這些變量所使用的記憶體都将在方法區中配置設定。

例如,在準備階段将value的值賦為0,在初始化的時候,設為123;

基本資料類型的零值

資料類型

零值

int

boolean

false

long

0L

float

0.0f

short

(short)0

double

0.0d

char

'u0000'

reference

null

byte

(byte)0

特殊情況:字段屬性為ConstantValue屬性,則會在準備階段指派,如

解析階段:虛拟機将常量池内的符号引用替換為直接引用的過程

符号引用:以一組符号來描述所引用的目标,符号可以是任意形式的字面量,符号引用目标不一定加載到記憶體中

直接引用:直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。引用目标必須已經在記憶體中。

解析目标:類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符

類初始化階段是類加載過程的最後一步,也是真正開始執行類中定義java程式代碼(或者說位元組碼)。

在準備階段,變量已經賦過一場系統要求的初始值,初始化階段根據程式員通過程式設定的值;

或者說:初始化階段是執行類構造器<clinit>()方法的過程。

<clinit>()方法由編譯器自動收集類中所有變量的指派動作和靜态語句塊(static{}塊)中語句合并産生的,編譯器收集順序由語句在

源檔案中出現的順序所決定的,靜态語句塊隻能通路到定義在靜态語句塊之前的變量;

定義在之後的變量,在前面的靜态語句塊可以指派,但不能通路。

<clinit>()方法與類的構造函數(或者說執行個體構造器<init>())不同,它不需要顯示地調用父構造器,虛拟機會保證在子類的<clinit>()

方法執行前,父類的<client>()方法已經執行完畢,是以第一個執行的<clinit>()方法類肯定是java.lang.Object

由于父類的<client>()方法先執行,也就是說父類中定義靜态語句塊優先于子類的變量指派操作。

<clinit>()如果一個類中沒有靜态語句塊和對變量指派的操作,則不會生成<clinit>()方法.

接口中不能有靜态語句塊,但與變量初始化指派操作,接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。

隻有當父接口中定義的變量使用時,父接口才會初始化。

接口的實作類在初始化是不執行接口的<clinit>()方法。

虛拟機會保證一個類的<clinit>()方法在多線程中被正确的加鎖,同步;如果有多個線程去初始化一個類,那麼隻有一個線程

去執行這個類的<clinit>()方法,其他線程都阻塞等待。

通過一個類的全限定名來擷取描述此類的二進制位元組流,這個動作在Java虛拟機外部實作,讓應用程式自己決定如何擷取所需的類,實作

這個動作的代碼子產品稱為類加載器。

對于任意一個類,都需要由加載他的類加載器和這個類本身一同确立在Java虛拟機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。

也就是說:比較兩個類是否“相等”,隻有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類源于同一個Class檔案,

被同一個虛拟機加載,隻要加載它們打的類加載器不同,這兩個類就比定不相等。

對于虛拟機來說,存在兩種類加載器:

啟動類加載器(Bootstrap ClassLoader),是虛拟機自身的一部分;

所有其他的類加載器,這些類加載器都由java語言實作,獨立于虛拟機外部,都繼承抽象類java.lang.ClassLoader

啟動類加載器:将放在<JAVA_HOME>lib目錄中的,或者被-XbootClassPath參數所指定的路徑中的,并且是虛拟機識别的類加載

到虛拟機記憶體中。無法被java程式直接引用

擴充類加載器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader實作,負責加載<JAVA_HOME>libext目錄的,

或者被java.ext.dirs系統變量所指定的路徑中的所有類,開發者可直接使用。

應用程式類加載器(Application ClassLoader):由sun.misc.Launcher$AppCLassLoader實作。這個類加載器是ClassLoader中

getSystemClassLoader()方法的傳回值,故也稱為系統加載器。負責加載使用者類路徑上所指定的類庫,開發者可直接使用,如果沒有

自定義自己的類加載器,這個就是預設的類加載器。

雙親委派模型:要求除頂層的啟動類加載器外,其他加載器都應當有自己的父加載器。

工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己嘗試加載這個類,而是把請求委派給父類加載器去完成,每一個層次的

類加載器都是如此,是以所有的加載請求最終都應該傳到頂層的啟動類加載器中,隻有當父加載器回報自己無法完成這個加載請求(

它的搜尋範圍中沒有找到所需的類)時,子類加載器才會嘗試自己加載。