JVM(五):探究類加載過程-上

JVM(五):探究類加載過程-上

本文咱們來研究一個Java字節碼文件(Class文件)是如何加載入內存中的,在這個過程當中涉及類加載過程當中的加載,驗證,準備,解析(鏈接),初始化,使用,銷燬過程,並探討實行這些過程的類加載器,以及其加載的邏輯。java

概述

Java擁有動態加載類和動態鏈接的特性,所以其加載過程並不像其餘語言在編譯時就已經完成,它是動態進行的,即在程序運行過程當中動態加載入內存中。數組

加載過程

類加載過程

在這裏須要記住的是,圖中的順序說明的是階段開始的順序,並非後面的階段須要等到前面的執行完成後纔可以執行,其在運行過程當中是一個交叉混合執行的過程。安全

此外解析階段也是一個特殊的階段,爲了支持Java語言的動態綁定,不少時候 Java 只要在運行後才能知道實際調用的對象是什麼,所以解析階段有時是開始在初始化後的。網絡

加載

加載階段完成的是將虛擬機外部的二進制字節流按照虛擬機所需的格式存儲在方法區之中。而爲了完成這步須要完成哪些功能呢:數據結構

  1. 經過一個類的全限定名獲取二進制流;
  2. 將二進制流定義的靜態存儲結構轉化爲方法區的運行時數據結構;
  3. 在內存中生成一個表明這個類的 Class 對象,做爲方法區數據的訪問接口。

須要注意的是,上面所說的3個步驟,都只是規範要求的部分,這個要求實際上是比較鬆的,不少東西並無限制的很死,好比說第一步的獲取二進制流,其並無要求二進制流必須從Class文件獲取,所以在使用過程當中類的二進制流能夠從網絡獲取,能夠動態計算生成等等。jvm

驗證

驗證做用是確保文件的字節流包含信息符合當前虛擬機要求,保證其並不會危害虛擬機的安全。由於之前說過 Class 文件並不都是源碼編譯而來的,人是能夠手動修改生成 Class 文件的,所以這一步驗證工做就十分有必要了。那麼驗證都須要驗證哪些地方呢:佈局

文件格式驗證優化

這一步主要是保證Class文件格式上符合Java信息的要求。例如文件類型,版本號,常量池,常量池數據等等。。。。。。3d

此外在這一步字節流就會進入內存的方法區之中了,後面的操做都是基於方法區內的存儲結構進行的。指針

元數據驗證

對字節碼描述信息進行語義分析,例如類是否有父類,重載是否正確,final,abstract有沒有用錯等,其主要目的是對類的元數據進行語義分析,保證符合Java語言規範。

字節碼驗證

對數據流和控制流進行分析。例如字節碼指令集的正確,程序跳轉的安全。其主要目的是檢查方法體內的數據安全,確保程序語義合法,符合邏輯。

符號引用驗證

符號引用驗證也是一個比較特殊的階段,其爲解析階段服務(這也驗證了前面所說的,這幾個過程並非依次執行完成的)。在解析過程當中,虛擬機將符號引用轉換爲直接引用,其主要是對常量池中的各類符號引用作匹配性校驗。檢驗內容包括如下幾個:

  1. 符號引用指向的類可否找到;
  2. 指定的類有沒有描述的方法和字段;
  3. 符號引用指向的各類信息的訪問權限是否是對的;
  4. 。。。。。。。。。。。。。。。

準備

類變量(被static修飾的變量)分配內存並設置類變量初始值。

這裏須要注意的是設初始值值得是爲其設置零值,例如數值量的 0,boolean 值的 false 等。可是特殊狀況下,如類變量是一個常量,那麼在準備階段,虛擬機就會將其設置爲常量指代的值。

解析

在驗證階段的符號引用驗證說過解析階段就是將符號引用轉換爲直接引用,那麼符號引用和直接引用分別指什麼呢,他們之間又有何區別:

  • 符號引用。是可以無歧義定位目標的任何形式的字面量,其與虛擬機實現的內存佈局無關,引用的目標不必定須要加載入內存中;
  • 直接引用。能夠直接指向目標指針,偏移量的引用,其和虛擬機實現的內存佈局相關,引用的目標必定須要在內存中。

在這一步虛擬機會將類/接口,字段,類方法,接口方法等進行解析,變爲直接引用。

初始化

初始化階段主要是初始化類變量和其餘資源,主要是經過<clint>()方法。

<clint>()是經過編譯器自動收集全部類變量的賦值動做和靜態語句塊(static{}塊)並按照順序合併生成的。

static塊能夠爲前面未定義的變量賦值,但沒法訪問

static{ 
        i = 111;
        // 下面語句沒法編譯經過,會提示Illegal forward reference
        //  System。out。println(i);
    }
    static int i = 0;
    public static void main(String[] args){
    System。out。println(i);`
    }

程序輸出爲0,由於其初始化操做是按照順序進行的,但若是這裏static int i;,不爲其賦值,那麼結果就是111。

虛擬機並無要求何時進行其餘階段的工做,但初始化階段不一樣。當發生一下幾種狀況時,虛擬機必需要開始初始化工做。(做爲初始化前的加載,驗證,準備,解析也就都循序漸進開始了)。

  1. 當在字節碼層面遇到如下指令時,new(對象都要生成了,確定要初始化了),get/put static(使用靜態變量了,確定要賦值了),invoke static(調用靜態方法了都,確定要爲靜態量賦值);
  2. 反射調用。當使用java。lang。reflect中的方法對類進行反射調用;
  3. 初始化一個類的時候,發現父類還有初始化,那麼須要先初始化其父類,(父接口不用當即初始化,只有使用到其常量時,才須要將其初始化);
  4. 虛擬機須要一個入口,所以主類須要初始化;
  5. 動態方法解析,解析出方法是其餘類的靜態方法,那麼須要將其初始化。

虛擬機規定有且僅有以上5種方法須要當即初始化,還有一些調用,看起來像須要初始化,但其實並不須要,能夠稱之爲被動調用

  1. 子類直接使用父類的靜態變量。虛擬機規定只有直接定義靜態變量的類須要初始化,所以這種狀況下,只會觸發父類的初始化,而子類並不會觸發;
  2. 數組對象。當定義對象數組時,只會觸發數組類的初始化,其內的對象的類並不會初始化;
  3. 當一個類調用另外一個類的常量時。此時並不會對常量類(被調用者)進行出初始化。只會將調用者初始化。由於在編譯階段,根據常量傳播優化,會將常量類的常量放置到調用者的常量池中,此時這兩個類已經沒有了瓜葛,所以也就不存在將其初始化了。

總結

在本文中着重介紹了一個類加載入內存中的各個階段過程,瞭解這個階段過程能夠明白虛擬機是如何將一個靜態的類文件,通過一系列的動做變爲 Java 內存中的各類數據結構。

在下一篇文章咱們將會介紹執行加載階段的主體,類加載器,明白類加載器的模型以及其背後的邏輯,並嘗試自定義一個類加載器,來完成加載工做。

iceWang公衆號

文章在公衆號 "iceWang" 第一手更新,有興趣的朋友能夠關注公衆號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!

本系列文章主要借鑑自《深刻分析 JavaWeb 技術內幕》和《深刻理解 Java 虛擬機- JVM 高級特性與最佳實踐》。

相關文章
相關標籤/搜索