search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

類載入機制

jvm

一、為什麼要使用類載入器?
Java語言里,類載入都是在程序運行期間完成的,這種策略雖然會令類載入時稍微增加一些性能開銷,但是會給java應用程序提供高度的靈活性。例如:
1.編寫一個面向介面的應用程序,可能等到運行時再指定其實現的子類;
2.用戶可以自定義一個類載入器,讓程序在運行時從網路或其他地方載入一個二進位流作為程序代碼的一部分;(這個是Android插件化,動態安裝更新apk的基礎)

二、類載入的過程

使用java編譯器可以把java代碼編譯為存儲位元組碼的Class文件,使用其他語言的編譯器一樣可以把程序代碼翻譯成Class文件,java虛擬機不關心Class的來源是何種語言。如圖所示:

在Class文件中描述的各種信息,最終都需要載入到虛擬機中才能運行和使用。那麼虛擬機是如何載入這些Class文件的呢?
JVM把描述類數據的位元組碼.Class文件載入到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的java類型,這就是虛擬機的類載入機制。

類從被載入到虛擬機內存中開始,到卸載出內存為止,它的生命周期包括了:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)七個階段,其中驗證、準備、解析三個部分統稱鏈接。

載入(裝載)、驗證、準備、初始化和卸載這五個階段順序是固定的,類的載入過程必須按照這種順序開始,而解析階段不一定;它在某些情況下可以在初始化之後再開始,這是為了運行時動態綁定特性(JIT例如介面只在調用的時候才知道具體實現的是哪個子類)。值得注意的是:這些階段通常都是互相交叉的混合式進行的,通常會在一個階段執行的過程中調用或激活另外一個階段。

1.載入:(重點)
載入階段是「類載入機制」中的一個階段,這個階段通常也被稱作「裝載」,主要完成:
1.通過「類全名」來獲取定義此類的二進位位元組流

2.將位元組流所代表的靜態存儲結構轉換為方法區的運行時數據結構

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

相對於類載入過程的其他階段,載入階段(準備地說,是載入階段中獲取類的二進位位元組流的動作)是開發期可控性最強的階段,因為載入階段可以使用系統提供的類載入器(ClassLoader)來完成,也可以由用戶自定義的類載入器完成,開發人員可以通過定義自己的類載入器去控制位元組流的獲取方式。

載入階段完成後,虛擬機外部的二進位位元組流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式有虛擬機實現自行定義,虛擬機並未規定此區域的具體數據結構。然後在java堆中實例化一個java.lang.Class類的對象,這個對象作為程序訪問方法區中的這些類型數據的外部介面。

2.驗證:(了解)

驗證是鏈接階段的第一步,這一步主要的目的是確保class文件的位元組流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身安全。
驗證階段主要包括四個檢驗過程:文件格式驗證、元數據驗證、位元組碼驗證和符號引用驗證。

1.文件格式驗證

驗證class文件格式規範,例如: class文件是否已魔術0xCAFEBABE開頭 , 主、次版本號是否在當前虛擬機處理範圍之內等

2.元數據驗證

這個階段是對位元組碼描述的信息進行語義分析,以保證起描述的信息符合java語言規範要求。驗證點可能包括:這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)、這個類是否繼承了不允許被繼承的類(被final修飾的)、如果這個類的父類是抽象類,是否實現了起父類或介面中要求實現的所有方法。

3.位元組碼驗證

進行數據流和控制流分析,這個階段對類的方法體進行校驗分析,這個階段的任務是保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為。如:保證訪法體中的類型轉換有效,例如可以把一個子類對象賦值給父類數據類型,這是安全的,但不能把一個父類對象賦值給子類數據類型、保證跳轉命令不會跳轉到方法體以外的位元組碼命令上。

4.符號引用驗證

符號引用中通過字元串描述的全限定名是否能找到對應的類、符號引用類中的類,欄位和方法的訪問性(private、protected、public、default)是否可被當前類訪問。

3.準備:(了解)

準備階段是正式為類變數分配內存並設置類變數初始值的階段,這些內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的知識點,首先是這時候進行內存分配的僅包括類變數(static 修飾的變數),而不包括實例變數,實例變數將會在對象實例化時隨著對象一起分配在java堆中。其次是這裡所說的初始值「通常情況」下是數據類型的零值,假設一個類變數定義為:

public static int value = 12;

那麼變數value在準備階段過後的初始值為0而不是12,因為這時候尚未開始執行任何java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放於類構造器方法之中,所以把value賦值為12的動作將在初始化階段才會被執行。

上面所說的「通常情況」下初始值是零值,那相對於一些特殊的情況,如果類欄位的欄位屬性表中存在ConstantValue屬性,那在準備階段變數value就會被初始化為ConstantValue屬性所指定的值,建設上面類變數value定義為:

public static final int value = 123;

編譯時javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value設置為123。

4.解析:(了解)
解析階段是虛擬機常量池內的符號引用替換為直接引用的過程。
符號引用:符號引用是一組符號來描述所引用的目標對象,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標對象並不一定已經載入到內存中。

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

虛擬機規範並沒有規定解析階段發生的具體時間,只要求了在執行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic這13個用於操作符號引用的位元組碼指令之前,先對它們使用的符號引用進行解析,所以虛擬機實現會根據需要來判斷,到底是在類被載入器載入時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用前才去解析它。

解析的動作主要針對類或介面、欄位、類方法、介面方法四類符號引用進行。分別對應編譯后常量池內的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四種常量類型。

1.類、介面的解析

2.欄位解析

3.類方法解析

4.介面方法解析

5.初始化:(了解)

類的初始化階段是類載入過程的最後一步,在準備階段,類變數已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序制定的主觀計劃去初始化類變數和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器方法的過程。在以下四種情況下初始化過程會被觸發執行:

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

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

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

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

在上面準備階段 public static int value = 12; 在準備階段完成後 value的值為0,而在初始化階調用了類構造器方法,這個階段完成後value的值為12。

*類構造器方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句快可以賦值,但是不能訪問。

*類構造器方法)不同,它不需要顯式調用父類構造,虛擬機會保證在子類方法已經執行完畢。因此在虛擬機中的第一個執行的方法的類肯定是java.lang.Object。*由於父類的方法先執行,也就意味著父類中定義的靜態語句快要優先於子類的變數賦值操作。方法對於類或介面來說並不是必須的,如果一個類中沒有靜態語句,也沒有變數賦值的操作,那麼編譯器可以不為這個類生成方法。*介面中不能使用靜態語句塊,但介面與類不太能夠的是,執行介面的方法。只有當父介面中定義的變數被使用時,父介面才會被初始化。另外,介面的實現類在初始化時也一樣不會執行介面的方法。*虛擬機會保證一個類的方法在多線程環境中被正確加鎖和同步,如果多個線程同時去初始化一個類,那麼只會有一個線程執行這個類的方法完畢。如果一個類的方法中有耗時很長的操作,那就可能造成多個進程阻塞。

三、類載入器

JVM設計者把類載入階段中的「通過'類全名'來獲取定義此類的二進位位元組流」這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱為「類載入器」。

1.類與類載入器

對於任何一個類,都需要由載入它的類載入器和這個類來確立其在JVM中的唯一性。也就是說,兩個類來源於同一個Class文件,並且被同一個類載入器載入,這兩個類才相等。

2.雙親委派模型

從虛擬機的角度來說,只存在兩種不同的類載入器:一種是啟動類載入器(Bootstrap ClassLoader),該類載入器使用C++語言實現,屬於虛擬機自身的一部分。另外一種就是所有其它的類載入器,這些類載入器是由Java語言實現,獨立於JVM外部,並且全部繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度來看,大部分Java程序一般會使用到以下三種系統提供的類載入器:
1)啟動類載入器(Bootstrap ClassLoader):負責載入JAVA_HOME\lib目錄中並且能被虛擬機識別的類庫到JVM內存中,如果名稱不符合的類庫即使放在lib目錄中也不會被載入。該類載入器無法被Java程序直接引用。
2)擴展類載入器(Extension ClassLoader):該載入器主要是負責載入JAVA_HOME\lib\,該載入器可以被開發者直接使用。
3)應用程序類載入器(Application ClassLoader):該類載入器也稱為系統類載入器,它負責載入用戶類路徑(Classpath)上所指定的類庫,開發者可以直接使用該類載入器,如果應用程序中沒有自定義過自己的類載入器,一般情況下這個就是程序中默認的類載入器。

我們的應用程序都是由這三類載入器互相配合進行載入的,我們也可以加入自己定義的類載入器。這些類載入器之間的關係如下圖所示:

如上圖所示的類載入器之間的這種層次關係,就稱為類載入器的雙親委派模型(Parent Delegation Model)。該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。子類載入器和父類載入器不是以繼承(Inheritance)的關係來實現,而是通過組合(Composition)關係來複用父載入器的代碼。


雙親委派模型的工作過程為:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的載入器都是如此,因此所有的類載入請求都會傳給頂層的啟動類載入器,只有當父載入器反饋自己無法完成該載入請求(該載入器的搜索範圍中沒有找到對應的類)時,子載入器才會嘗試自己去載入。


使用這種模型來組織類載入器之間的關係的好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如java.lang.Object類,無論哪個類載入器去載入該類,最終都是由啟動類載入器進行載入,因此Object類在程序的各種類載入器環境中都是同一個類。否則的話,如果不使用該模型的話,如果用戶自定義一個java.lang.Object類且存放在classpath中,那麼系統中將會出現多個Object類,應用程序也會變得很混亂。如果我們自定義一個rt.jar中已有類的同名Java類,會發現JVM可以正常編譯,但該類永遠無法被載入運行。
在rt.jar包中的java.lang.ClassLoader類中,我們可以查看類載入實現過程的代碼,具體源碼如下:



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦