深入理解Java虛擬機(二)類的加載過程

? ? ? ?本篇文章主要介紹一下虛擬機是如何進行類加載的以及進行類加載的加載器的工作原理。

一、類加載過程

Java的類加載過程分為三個主要步驟:加載、鏈接、初始化。

圖1

1.加載

將class二進制文件加載到內(nèi)存中,通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。在加載過程中虛擬機將字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。在java堆中生成一個代表這個類的java.lang.Class對象,做為方法區(qū)這些數(shù)據(jù)的訪問入口。加載階段完成之后二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)中。

在這個加載過程中虛擬機不能獨立完成,需要借助加載器完成的,本篇后面會對加載器的加載原理進行介紹。

2.驗證

這是虛擬機安全的重要保障,JVM需要核驗字節(jié)信息是符合Java虛擬機規(guī)范的,否則就被認為是VerifyError,這樣就防止了惡意信息或者不合規(guī)的信息危害JVM的運行,驗證階段有可能觸發(fā)更多class的加載。驗證階段主要進行了如下內(nèi)容的驗證:

(1)文件格式驗證:驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當前版本的虛擬機處理。

(2)元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析,以確保其描述的信息符合java語言規(guī)范的要求。

(3)字節(jié)碼驗證:這個階段的主要工作是進行數(shù)據(jù)流和控制流的分析。任務是確保被驗證類的方法在運行時不會做出危害虛擬機安全的行為。

(4)符號引用驗證:這一階段發(fā)生在虛擬機將符號引用轉(zhuǎn)換為直接引用的時候(解析階段),主要是對類自身以外的信息進行匹配性的校驗。目的是確保解析動作能夠正常執(zhí)行。

3.準備

對靜態(tài)變量賦默認值,而不是初始值(目標值),準備階段是正式為靜態(tài)變量分配內(nèi)存并設置初始值,這些內(nèi)存都將在方法區(qū)中進行分配,這里的變量僅包括類變量(靜態(tài)變量)不包括實例(成員)變量。不同的數(shù)據(jù)類型及其初始值如下圖所示。

圖2

public class LinkedPrepare {

? ? private static int a = 10; //①

? ? private final static int b = 10; //②

}?

其中static int a=10在準備階段不是10,而是初始值0,當然final static int b則還會是10,為什么呢?因為final修飾的靜態(tài)變量(可直接計算得出結(jié)果)不會導致類的初始化,是一種被動引用,因此就不存在連接階段了。當然了更加嚴謹?shù)慕忉屖莊inal static int b=10在類的編譯階段javac會將其value生成一個ConstantValue屬性,直接賦予10。

4.解析

解析是虛擬機將常量池的符號引用替換為直接引用的過程。

5.初始化

類的初始化階段是整個類加載過程中的最后一個階段,在初始化階段做的最主要的一件事情就是執(zhí)行<clinit>()方法的過程(clinit是class initialize前面幾個字母的簡寫)在<clinit>()方法中所有的類變量都會被賦予正確的值,也就是在程序編寫的時候指定的值。<clinit>()方法是在編譯階段生成的,也就是說它已經(jīng)包含在了class文件中了,<clinit>()中包含了所有類變量的賦值動作和靜態(tài)語句塊的執(zhí)行代碼,編譯器收集的順序是由執(zhí)行語句在源文件中的出現(xiàn)順序所決定的。另外需要注意的一點是,靜態(tài)語句塊只能對后面的靜態(tài)變量進行賦值,但是不能對其進行訪問。

另外<clinit>()方法與類的構(gòu)造函數(shù)有所不同,它不需要顯示的調(diào)用父類的構(gòu)造器,虛擬機會保證父類的<clinit>()方法最先執(zhí)行,因此父類的靜態(tài)變量總是能夠得到優(yōu)先賦值。

雖然說Java編譯器會幫助class生成<clinit>()方法,但是該方法并不意味著總是會生成,比如某個類中既沒有靜態(tài)代碼塊,也沒有靜態(tài)變量,那么它就沒有生成<clinit>()方法的必要了,接口中同樣也是如此,由于接口天生不能定義靜態(tài)代碼塊,因此只有當接口中有變量的初始化操作時才會生成<clinit>()方法。

二、雙親委派模式

在類加載過程中,是需要加載器將class二進制文件加載到內(nèi)存中,JVM預定義的三種類型類加載器:

(1)啟動類加載器(Bootstrap Class-Loader):是用本地代碼實現(xiàn)的類裝入器,它負責將 <Java_Runtime_Home>/lib下面的類庫加載到內(nèi)存中(比如rt.jar)。由于引導類加載器涉及到虛擬機本地實現(xiàn)細節(jié),開發(fā)者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。

(2)擴展類加載器(Extension Class-Loader):是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現(xiàn)的。它負責將<Java_Runtime_Home>/lib/ext或者由系統(tǒng)變量 java.ext.dir指定位置中的類庫加載到內(nèi)存中。開發(fā)者可以直接使用標準擴展類加載器。

(3)應用程序(App)類加載器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現(xiàn)的。它負責將系統(tǒng)類路徑(CLASSPATH)中指定的類庫加載到內(nèi)存中。開發(fā)者可以直接使用應用程序類加載器。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般稱為系統(tǒng)(System)加載器。

圖3

雙親委托模型的主要工作過程:

如果一個類加載器收到了類加載請求,它首先不會自己去嘗試加載這個類,而是把請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求時(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

使用雙親委派模型來組織類加載器之間的關(guān)系,使得Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。例如類java.lang.String,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都要委托給處于模型最頂端的啟動類加載器進行加載,因此String類在程序的各種類加載器環(huán)境中都是同一個類。相反如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.String的類,并放在程序的ClassPath中,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,Java類型體系中最基礎的行為也就無法保證,應用程序也將會一片混亂。

JVM在搜索類的時候,是如何判定兩個class是相同的?

JVM在判定兩個class是否相同時,不僅要判斷兩個全限定類名是否相同,而且要判斷是否由同一個類加載器實例加載的。只有兩者同時滿足的情況下,JVM才認為這兩個class是相同的。就算兩個class是同一份class字節(jié)碼,如果被兩個不同的ClassLoader實例所加載,JVM也會認為它們是兩個不同class。

最后:打一個小廣告,后續(xù)的文章會在微信公眾號“程序員之家QAQ”推送,歡迎大家搜索關(guān)注~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容