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

通過前面好幾章的或詳細或不詳細的介紹,我們終於把字節碼的結構分析的差不多了。現在我們面臨這樣一個問題,如何運行一個字節碼文件呢?

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

類的生命週期

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


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




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

加載這個過程主要是通過一個類的權限定名獲取類的二進制字節流

然後把字節流轉化爲一種方法區運行時的數據結構

最後在內存中形成一個Class對象

圖示:


這裏面我們可操作性比較強的也就是加載字節碼這個過程了,我們都很熟悉一個方法叫做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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章