JVM類加載機制該如何解析-創(chuàng)新互聯(lián)

JVM類加載機制該如何解析,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。

創(chuàng)新互聯(lián)公司長期為上1000+客戶提供的網站建設服務,團隊從業(yè)經驗10年,關注不同地域、不同群體,并針對不同對象提供差異化的產品和服務;打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網生態(tài)環(huán)境。為平安企業(yè)提供專業(yè)的網站設計制作、網站制作,平安網站改版等技術服務。擁有十載豐富建站經驗和眾多成功案例,為您定制開發(fā)。

概述

虛擬機把描述類的數(shù)據從Class文件加載到內存,并對數(shù)據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

與那些在編譯時需要進行鏈接工作的語言不同,在Java語言里,類型的加載、連接和初始化過程都是在程序運行期間完成的,例如import java.util.*下面包含很多類,但是,在程序運行的時候,虛擬機只會加載哪些我們程序需要的類。這種策略雖然會令類加載時稍微增加一些性能開銷,但是會為Java應用程序提供高度的靈活性。

類加載的時機

類從創(chuàng)建起(這里的類也可能是接口,下同),就注定了其是有生命周期的(這里的生命周期指的是類在運行期間所經歷的過程,與是否存儲在存儲介質上無關)。類從被虛擬機加載到內存中開始,到卸載出內存為止,它的生命周期經歷了加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading),一共七個階段,其中驗證、準備、解析部分統(tǒng)稱為連接。這七個階段可以用如下圖描述:

從上圖中可以明顯看出各個階段是有順序的,加載、驗證、準備、初始化這個5個階段的順序是固定的,也就是說類的加載過程必須按照這種順序按部就班開始;解析階段則不一定,解析階段的工作完全可能在初始化之后才開始,之所以這么設計,就是為了支持Java語言的動態(tài)綁定。還有一點需要注意的是,雖然上述的5個階段可能按照順序開始,但是并不是說一個接一個階段完成后才開始,一個階段的進行完全可能激活另一個階段的進行,交叉混合式的進行。

那么什么情況下需要開始類加載過程的第一個階段,加載到內存中呢?這就不得不涉及兩個概念:主動引用和被動引用。根據Java虛擬機的規(guī)范,只有5中情況屬于主動引用:

    遇到new(使用new 關鍵字實例化一個對象)、getstatic(讀取一個類的靜態(tài)字段)、putstatic或者invokestatic(設置一個類的靜態(tài)字段)這4條指令的時候,如果累沒有進行過初始化。則需要先觸發(fā)其初始化。

    使用反射進行反射調用的時候,如果類沒有初始化,則需要先觸發(fā)其初始化。

    當初始化一個類的時候,如果其父類沒有初始化,則需要先觸發(fā)其父類的初始化

    程序啟動需要觸發(fā)main方法的時候,虛擬機會先觸發(fā)這個類的初始化

    當使用jdk1.7的動態(tài)語言支持的時候,如果一個java.lang.invoke.MethodHandler實例最后的解析結果為REF_getStatic、REF_pusStatic、REF_invokeStatic的方法句柄(句柄中包含了對象的實例數(shù)據和類型數(shù)據,句柄是訪問對象的一種方法。句柄存儲在堆中),并且句柄對應的類沒有被初始化,那么需要先觸發(fā)這個類的初始化。

5種之外情況就是被動引用。被動引用的經典例子有:

    通過子類引用父類的靜態(tài)字段   這種情況不會導致子類的初始化,因為對于靜態(tài)字段,只有直接定義靜態(tài)字段的類才會被觸發(fā)初始化,子類不是定義這個靜態(tài)字段的類,自然不能被實例化。

    通過數(shù)組定義來引用類,不會觸發(fā)該類的初始化   例如, Clazz[] arr = new Clazz[10];并不會觸發(fā)。

    常量不會觸發(fā)定義常量的類的初始化   因為常量在編譯階段會存入調用常量的類的常量池中,本質上并沒有引用定義這個常量的類,所以不會觸發(fā)定義這個常量的類的初始化。

對于這5種主動引用會觸發(fā)類進行初始化的場景,在java虛擬機規(guī)范中限定了“有且只有”這5種場景會觸發(fā)類的加載。

類加載的過程

加載

在加載階段虛擬機需要完成以下三件事:

    通過一個類的全限定名稱來獲取此類的二進制字節(jié)流

    將這個字節(jié)流所代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數(shù)據結構

    在內存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據的訪問入口

這三件事在Java虛擬機中并沒有說的很詳細,比如類的全限定名稱是如何加載進來的,以及從哪里加載進來的。通常來講,一個類的全限定名稱可以從zip、jar包中加載,也可以從網絡中獲取,也可以在運行的時候生成(這點最明顯的技術體現(xiàn)就是反射機制)。

對于類的加載,可以分為數(shù)組類型和非數(shù)組類型,對于非數(shù)組類型可以通過系統(tǒng)的引導類加載器進行加載,也可以通過自定義的類加載器進行加載。這點是比較靈活的。而對于數(shù)組類型,數(shù)組類本身不通過類加載器進行加載,而是通過Java虛擬機直接進行加載的,那么是不是數(shù)組類型的類就不需要類加載器了呢?答案是否定的。因為當數(shù)組去除所有維度之后的類型最終還是要依靠類加載器進行加載的,所以數(shù)組類型的類與類加載器的關系還是很密切的。

通常一個數(shù)組類型的類進行加載需要遵循以下的原則:

    如果數(shù)組的組件類型(也就是數(shù)組類去除一個維度之后的類型,比如對于二維數(shù)組,去除一個維度之后是一個一維數(shù)組)是引用類型,那么遞歸采用上面的過程加載這個組件類型

    如果數(shù)組類的組件類型不是引用類型,比如是基本數(shù)據類型,Java虛擬機將把數(shù)組類標記為與引導類加載器關聯(lián)

    數(shù)組類的可見性與組件類型的可見性是一致的。如果組件類型不是引用類型,那么數(shù)組類的可見性是public,意味著組件類型的可見性也是public。

前面已經介紹過,加載階段與連接階段是交叉進行的,所以可能加載階段還沒有完成,連接階段就已經開始。但是即便如此,記載階段與連接階段之間的開始順序仍然保持著固定的順序。

驗證

驗證階段的目的是為了確保Class字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機的安全。

我們知道Java語言具有相對的安全性(這里的安全性體現(xiàn)為兩個方面:一是Java語言本身特性,比如Java去除指針,這點可以避免對內存的直接操作;二是Java所提供的沙箱運行機制,Java保證所運行的機制都是在沙箱之內運行的,而沙箱之外的操作都不可以運行)。但是需要注意的是Java虛擬機處理的Class文件并不一定是是從Java代碼編譯而來,完全可能是來自其他的語言,甚至可以直接通過十六進制編輯器書寫Class文件(當然前提是編寫的Class文件符合規(guī)范)。從這個角度講,其他來源的Class文件是不可能都保證其安全性的。所以如果Java虛擬機都信任其加載進來的Class文件,那么很有可能會造成對虛擬機自身的危害。

虛擬機的驗證階段主要完后以下4項驗證:文件格式驗證、元數(shù)據驗證、字節(jié)碼驗證、符號引用驗證。(結合前文,查看Class類文件結構)

文件格式驗證

這里的文件格式是指Class的文件規(guī)范,這一步的驗證主要保證加載的字節(jié)流(在計算機中不可能是整個Class文件,只有0和1,也就是字節(jié)流)符合Class文件的規(guī)范(根據前面對Class類文件的描述,Class文件的每一個字節(jié)表示的含義都是確定的。比如前四個字節(jié)是否是一個魔數(shù)等)以及保證這個字節(jié)流可以被虛擬機接受處理。

在Hotspot的規(guī)范中,對文件格式的驗證遠不止這些,但是只有通過文件格式的驗證才能進入方法區(qū)中進行存儲。所以自然也就知道,后面階段的驗證工作都是在方法區(qū)中進行的。

元數(shù)據驗證

元數(shù)據可以理解為描述數(shù)據的數(shù)據,更通俗的說,元數(shù)據是描述類之間的依賴關系的數(shù)據,比如Java語言中的注解使用(使用@interface創(chuàng)建一個注解)。元數(shù)據驗證主要目的是對類的元數(shù)據信息進行語義校驗,保證不存在不符合Java語言規(guī)范(Java語法)的元數(shù)據信息。

具體的驗證信息包括以下幾個方面:

    這個類是否有父類(除了java.lang.Object外其余的類都應該有父類)

    這個類的父類是否繼承了不允許被繼承的類(比如被final修飾的類)

    如果這個類不是抽象類,是否實現(xiàn)了其父類或者接口中要求實現(xiàn)的方法

    類中的字段、方法是否與父類產生矛盾(比如是否覆蓋了父類的final字段)

字節(jié)碼驗證

這個階段主要對類的方法體進行校驗分析。通過了字節(jié)碼的驗證并不代表就是沒有問題的,但是如果沒有通過驗證就一定是有問題的。整個字節(jié)碼的驗證過程比這個復雜的多,由于字節(jié)碼驗證的高度復雜性,在jdk1.6版本之后的虛擬機增加了一項優(yōu)化,Class類文件結構這篇文章中說到過有一個屬性:StackMapTable屬性??梢院唵卫斫膺@個屬性是用于檢查類型是否匹配。

符號引用驗證

這個驗證是最后階段的驗證,符號引用是Class文件的邏輯符號,直接引用指向的方法區(qū)中某一個地址,在解析階段,將符號引用轉為直接引用,這里只進行轉化前的匹配性校驗。符號引用驗證主要是對類自身以外的信息進行匹配性校驗。比如符號引用是否通過字符串描述的全限定名是否能夠找到對應點類。

符號引用(Symbolic Reference)   符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可(符號字面量,還沒有涉及到內存)。符號引用與虛擬機實現(xiàn)的內存布局無關,引用的目標并不一定已經加載在內存中。各種虛擬機實現(xiàn)的內存布局可以各不相同,但是他們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規(guī)范的Class文件格式中。

直接引用(Direct Reference)   直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄(可以理解為內存地址)。直接引用是與虛擬機實現(xiàn)的內存布局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標必定已經在內存中存在。

進行符號引用驗證的目的在于確保解析動作能夠正常執(zhí)行,如果無法通過符號引用驗證那么將會拋出java.lang.IncomingChangeError異常的子類。

準備

完成了驗證階段之后,就進入準備階段。準備階段是正式為變量分配內存空間并且設置類變量初始值

需要注意的是,這時候進行內存分配的僅僅是類變量(也就是被static修飾的變量),實例變量是不包括的,實例變量的初始化是在對象實例化的時候進行初始化,而且分配的內存區(qū)域是Java堆。這里的初始值也就是在編程中默認值,也就是零值。

例如public static int value = 123 ;value在準備階段后的初始值是0而不是123,因為此時尚未執(zhí)行任何的Java方法,而把value賦值為123的putStatic指令是程序被編譯后,存放在類構造器clinit()方法之中,把value賦值為123的動作將在初始化階段才會執(zhí)行。

特殊情況:如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量就會被初始化為ConstantValue屬性所指定的值,例如public static final int value = 123 編譯時javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將變量賦值為123。

解析

解析階段是將常量池中的符號引用替換為直接引用的過程(前面已經提到了符號引用與直接引用的區(qū)別)。在進行解析之前需要對符號引用進行解析,不同虛擬機實現(xiàn)可以根據需要判斷到底是在類被加載器加載的時候對常量池的符號引用進行解析(也就是初始化之前),還是等到一個符號引用被使用之前進行解析(也就是在初始化之后)。

到現(xiàn)在我們已經明白解析階段的時機,那么還有一個問題是:如果一個符號引用進行多次解析請求,虛擬機中除了invokedynamic指令外,虛擬機可以對第一次解析的結果進行緩存(在運行時常量池中記錄引用,并把常量標識為一解析狀態(tài)),這樣就避免了一個符號引用的多次解析。

解析動作主要針對的是類或者接口、字段、類方法、方法類型、方法句柄和調用點限定符7類符號引用。這里主要說明前四種的解析過程。

類或者接口解析

要把一個類或者接口的符號引用解析為直接引用,需要以下三個步驟:

    如果該符號引用不是一個數(shù)組類型,那么虛擬機將會把該符號代表的全限定名稱傳遞給調用這個符號引用的類。這個過程由于涉及驗證過程所以可能會觸發(fā)其他相關類的加載

    如果該符號引用是一個數(shù)組類型,并且該數(shù)組的元素類型是對象。我們知道符號引用是存在方法區(qū)的常量池中的,該符號引用的描述符會類似”[java/lang/Integer”的形式(描述符的概念詳見前文【深入理解JVM】:Class類文件結構),將會按照上面的規(guī)則進行加載,虛擬機將會生成一個代表此數(shù)組對象的直接引用

    如果上面的步驟都沒有出現(xiàn)異常,那么該符號引用已經在虛擬機中產生了一個直接引用,但是在解析完成之前需要對符號引用進行驗證,主要是確認當前調用這個符號引用的類是否具有訪問權限,如果沒有訪問權限將拋出java.lang.IllegalAccess異常

字段解析

對字段的解析需要首先對其所屬的類進行解析,因為字段是屬于類的,只有在正確解析得到其類的正確的直接引用才能繼續(xù)對字段的解析。對字段的解析主要包括以下幾個步驟:

    如果該字段符號引用(后面簡稱符號)就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,解析結束

    否則,如果在該符號的類實現(xiàn)了接口,將會按照繼承關系從下往上遞歸搜索各個接口和它的父接口,如果在接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,那么久直接返回這個字段的直接引用,解析結束

    否則,如果該符號所在的類不是Object類的話,將會按照繼承關系從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都相匹配的字段,那么直接返回這個字段的直接引用,解析結束

    否則,解析失敗,拋出java.lang.NoSuchFieldError異常   如果最終返回了這個字段的直接引用,就進行權限驗證,如果發(fā)現(xiàn)不具備對字段的訪問權限,將拋出java.lang.IllegalAccessError異常

類方法解析

進行類方法的解析仍然需要先解析此類方法的類,在正確解析之后需要進行如下的步驟:

    類方法和接口方法的符號引用是分開的,所以如果在類方法表中發(fā)現(xiàn)class_index(類中方法的符號引用)的索引是一個接口,那么會拋出java.lang.IncompatibleClassChangeError的異常

    如果class_index的索引確實是一個類,那么在該類中查找是否有簡單名稱和描述符都與目標字段相匹配的方法,如果有的話就返回這個方法的直接引用,查找結束

    否則,在該類的父類中遞歸查找是否具有簡單名稱和描述符都與目標字段相匹配的字段,如果有,則直接返回這個字段的直接引用,查找結束

    否則,在這個類的接口以及它的父接口中遞歸查找,如果找到的話就說明這個方法是一個抽象類,查找結束,返回java.lang.AbstractMethodError異常(因為抽象類是沒有實現(xiàn)的)

    否則,查找失敗,拋出java.lang.NoSuchMethodError異常   如果最終返回了直接引用,還需要對該符號引用進行權限驗證,如果沒有訪問權限,就拋出java.lang.IllegalAccessError異常

接口方法解析

同類方法解析一樣,也需要先解析出該方法的類或者接口的符號引用,如果解析成功,就進行下面的解析工作:

    如果在接口方法表中發(fā)現(xiàn)class_index的索引是一個類而不是一個接口,那么也會拋出java.lang.IncompatibleClassChangeError的異常

    否則,在該接口方法的所屬的接口中查找是否具有簡單名稱和描述符都與目標字段相匹配的方法,如果有的話就直接返回這個方法的直接引用。查找結束

    否則,在該接口以及其父接口中查找,直到Object類,如果找到則直接返回這個方法的直接引用   否則,查找失敗

接口的所有方法都是public,所以不存在訪問權限問題

初始化

到了初始化階段,虛擬機才開始真正執(zhí)行Java程序代碼,前文講到對類變量的初始化,但那是僅僅賦初值,用戶自定義的值還沒有賦給該變量。只有到了初始化階段,才開始真正執(zhí)行這個自定義的過程,所以也可以說初始化階段是執(zhí)行類構造器方法clinit() 的過程。那么這個clinit() 方法是這么生成的呢?

clinit() 是編譯器自動收集類中所有類變量的賦值動作和靜態(tài)語句塊合并生成的。編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的。靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問。   示例代碼:

public class Test {  static{   i =0;   //給變量賦值可以正常編譯通過   System.out.println(i); //這句編譯器會提示“非法向前引用”  }  static int i = 1;}

clinit() 方法與類的構造器方法不同,因為前者不需要顯式調用父類構造器,因為虛擬機會保證在子類的clinit() 方法執(zhí)行之前,父類的clinit() 方法已經執(zhí)行完畢

由于父類的clinit() 方法會先執(zhí)行,所以就表示父類的static方法會先于子類的clinit() 方法執(zhí)行。如下面的例子所示,輸出結果為2而不是1。

public class Parent {  public static int A = 1;  static{   A = 2;  } } public class Sub extends Parent{  public static int B = A; } public class Test {  public static void main(String[] args) {   System.out.println(Sub.B);  } }

clinit()方法對于類或者接口來說并不是必需的,如果一個類中沒有靜態(tài)語句塊也沒有對變量的賦值操作,那么編譯器可以不為這個類生成clinit()方法。

接口中不能使用靜態(tài)語句塊,但仍然有變量賦值的初始化操作,因此接口也會生成clinit()方法。但是接口與類不同,執(zhí)行接口的clinit()方法不需要先執(zhí)行父接口的clini>()方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現(xiàn)類在初始化時也不會執(zhí)行接口的clinit()方法。

虛擬機會保證一個類的clinit()方法在多線程環(huán)境中被正確地加鎖和同步。如果有多個線程去同時初始化一個類,那么只會有一個線程去執(zhí)行這個類的clinit()方法,其它線程都需要阻塞等待,直到活動線程執(zhí)行clinit()方法完畢。如果在一個類的clinit()方法中有耗時很長的操作,那么就可能造成多個進程阻塞。

注意:解析和初始化在繼承關系中從下往上遞歸搜索父類的特性,可以用來解釋繼承關系中的父類和子類的初始化順序(另一個原因是Java HotSpot虛擬機的內存布局分配策略的影響)。

看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝您對創(chuàng)新互聯(lián)網站建設公司,的支持。

當前名稱:JVM類加載機制該如何解析-創(chuàng)新互聯(lián)
文章路徑:http://www.muchs.cn/article18/ceoddp.html

成都網站建設公司_創(chuàng)新互聯(lián),為您提供網站內鏈云服務器、服務器托管、網站改版、網站營銷營銷型網站建設

廣告

聲明:本網站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

成都網站建設