天天看點

[jvm解析系列][九]類的加載過程和類的初始化。你的類該怎麼執行?為什麼需要ClassLoader?

通過前面好幾章的或詳細或不詳細的介紹,我們終于把位元組碼的結構分析的差不多了。現在我們面臨這樣一個問題,如何運作一個位元組碼檔案呢?

首先,java語言不同于其他的編譯時需要進行連結工作的語言不通,java語言有一個很明顯的特性,那就是動态加載,一個位元組碼的加載往往都是在程式運作的時候加載進來的,很多時候這種方式給我們帶來了便利。雖然從某種意義上來說他可能消耗了一定的資源降低了性能。

類的生命周期?

沒錯,一個類的生命周期,在很多人眼裡可能類天生都擺在那裡了,随着程式生,随着程式死。但是事實情況并不是這樣,java語言的動态加載要求了一個類肯定有他的生命周期,一個類的生命周期有7個階段,如下圖所示:

[jvm解析系列][九]類的加載過程和類的初始化。你的類該怎麼執行?為什麼需要ClassLoader?

這個圖裡第二行我特意放了三個并排就是因為第二行可以統稱為連結,這中間遵循了嚴格的開始順序,但是解析是有可能在初始化之後開始,這也是為了java語言的動态綁定而做的改變。并且這中間隻有開始的先後關系,沒有結束的先後關系。

首先我們說一說第一個情況加載:什麼時候開始一個類的加載過程,jvm規範沒有規定,不同的虛拟機有不同的實作。

加載這個過程主要是通過一個類的權限定名擷取類的二進制位元組流

然後把位元組流轉化為一種方法區運作時的資料結構

最後在記憶體中形成一個Class對象

圖示:

[jvm解析系列][九]類的加載過程和類的初始化。你的類該怎麼執行?為什麼需要ClassLoader?

這裡面我們可操作性比較強的也就是加載位元組碼這個過程了,我們都很熟悉一個方法叫做loadClass,沒錯他就是用來加載位元組碼的,我們可以自定義一個類加載器重寫loadClass方法去控制位元組流的加載方式,于是我們可以從網絡上擷取,從資料庫擷取,從檔案中擷取,還有一種動态代理技術是通過計算生成的。

加載完成後就是驗證了。

驗證主要是確定Class檔案裡沒有坑爹的東西,不會損害虛拟機自身。大概就是分為4個驗證流程。

1、檔案格式驗證:我們之前講過的魔數和大小version就是在這個位置驗證的,常量池中是不是所有類型都支援,Constant_utf8_info是不是都是UTF8編碼等等。這一階段的目的主要是驗證輸入的位元組流能夠正确的解析,保證格式上的正确,如果通過了這個驗證就把位元組流加入到了方法區中,下面的驗證都是對方法區的驗證。(給幾個名詞連結魔數,大小版本号,方法區)

2、中繼資料驗證:這一個階段主要是看看中繼資料是否符合語義,像父類是否繼承了不被允許的類(final類),是不是實作了父類和接口中所有要實作的方法等。

3、位元組碼驗證;這個階段的驗證,主要是看看邏輯上有沒有錯誤,比如有一個跳轉指令跳到了方法的外部這種。

4、符号引用驗證:這個大家應該很常見,比較常見的就是是不是能通路到某個類(不是private和protected),通過字元串描述的權限定名能不能找到對應的類。

在驗證之後,我們就會開始準備。

在準備階段裡,會為類變量配置設定記憶體并且設定類變量(static修飾的變量)的初始值,而且諸如static int a = 1;這種情況,在準備階段是不會指派1的。而是指派最基本的初始值0,因為1需要時初始化的時候在類的構造器中調用。

但是,如果字段變量被final修飾,這個字段表就會存在一個ConstantValue屬性(詳見ConstantValue),在這個時候這個變量就會被指派,如static final int a = 1;這時候a就會被指派為1;

在準備之後,jvm會開始解析過程。

解析在通俗意義上講就是把常量池裡的符号引用替換成直接引用。首先我們來解釋一下這兩個名詞的意思

1、符号引用:符号引用就是指使用一組符号來進行引用,被引用的目标不一定加載到記憶體中

2、直接引用:直接引用是指直接指向目标的指針,相對偏移量或是句柄。

對于解析,jvm并沒有具體規定什麼時候執行,隻要在操作符号引用之前進行解析就可以了。是以具體的實作還要看jvm是怎麼設計的(在加載時解析還是在使用前解析)

接下來就是初始化了

我們之前說過,一個類的加載時間是要依靠具體的虛拟機而定,但是遇到主動引用時加載驗證準備工作必須完結

有五種情況,我們把這五種情況叫做主動引用。(這之前已經進行了加載驗證準備,遇到下面情況直接初始化即可)

1、遇到new、getstatic、putstatic和invokestatic這4條位元組碼時,對應到java語言就是new了一個類,調用了一個類的靜态字段(被final修飾的已經在常量池了,不算)

2、使用java.lang.reflect包對類進行反射條用的時候。

3、初始化一個類,他的父類沒有進行過初始化的時候,需要先初始化他的父類。

4、虛拟機啟動的那個執行類(也就是帶有public void main(String[] args){}方法的那個類)

5、jdk1.7之後新增特性動态語言支援,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個類沒有初始化的時候。

當然除了這五個主動引用以外,其餘所有方法都不會出發初始化,我們叫做被動引用,下面舉幾個例子。

1、通過子類引用父類的靜态字段,不會觸發子類的初始化。

2、通過數組定義來引用類,不會觸發類的初始化,他把原來的類翻譯成了另外一個類,這個類帶有一些數組元素的通路方法。

3、常量不會觸發類的初始化。

初始化的過程:

在初始化的時候就是真正開始執行java代碼的時候了,這個時候會執行一個叫做類構造器的東西(<client>())他負責收集類中所有的static{}然後合并,順序和原順序一樣,在static{}中隻能通路定義在靜态語句塊之前的變量,對于後面的變量隻能指派不能通路。

我們之前就講過在初始化一個類的時候,如果他的父類沒有進行過初始化,則需要先初始化他的父類。于是父類的<client>()方法肯定優于子類執行,也就是說父類的靜态代碼塊優于子類的靜态代碼塊執行。但是接口的<client>()方法并不一定先于子類方法執行,因為父接口的<client>()方法是在調用時才執行的。

于是我們可以看出來,在整個加載過程中,程式員可以操作的部分僅僅隻有ClassLoader(加載位元組碼)和初始化靜态代碼塊部分。是以我們接下來就會講ClassLoader