1. 程式人生 > >JVM(五):探究類載入過程-上

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

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

本文我們來研究一個Java位元組碼檔案(Class檔案)是如何載入入記憶體中的,在這個過程中涉及類載入過程中的載入,驗證,準備,解析(連線),初始化,使用,銷燬過程,並探討實行這些過程的類載入器,以及其載入的邏輯。

概述

Java擁有動態載入類和動態連線的特性,因此其載入過程並不像其他語言在編譯時就已經完成,它是動態進行的,即在程式執行過程中動態載入入記憶體中。

載入過程

在這裡需要記住的是,圖中的順序說明的是階段開始的順序,並不是後面的階段需要等到前面的執行完成後才能夠執行,其在執行過程中是一個交叉混合執行的過程。

此外解析階段也是一個特殊的階段,為了支援Java語言的動態繫結,很多時候 Java 只要在執行後才能知道實際呼叫的物件是什麼,因此解析階段有時是開始在初始化後的。

載入

載入階段完成的是將虛擬機器外部的二進位制位元組流按照虛擬機器所需的格式儲存在方法區之中。而為了完成這步需要完成哪些功能呢:

  1. 通過一個類的全限定名獲取二進位制流;
  2. 將二進位制流定義的靜態儲存結構轉化為方法區的執行時資料結構;
  3. 在記憶體中生成一個代表這個類的 Class 物件,作為方法區資料的訪問介面。

需要注意的是,上面所說的3個步驟,都只是規範要求的部分,這個要求其實是比較鬆的,很多東西並沒有限制的很死,比如說第一步的獲取二進位制流,其並沒有要求二進位制流必須從Class檔案獲取,因此在使用過程中類的二進位制流可以從網路獲取,可以動態計算生成等等。

驗證

驗證作用是確保檔案的位元組流包含資訊符合當前虛擬機器要求,保證其並不會危害虛擬機器的安全。因為以前說過 Class 檔案並不都是原始碼編譯而來的,人是可以手動修改生成 Class 檔案的,因此這一步驗證工作就十分有必要了。那麼驗證都需要驗證哪些地方呢:

檔案格式驗證

這一步主要是保證Class檔案格式上符合Java資訊的要求。例如檔案型別,版本號,常量池,常量池資料等等。。。。。。

此外在這一步位元組流就會進入記憶體的方法區之中了,後面的操作都是基於方法區內的儲存結構進行的。

元資料驗證

對位元組碼描述資訊進行語義分析,例如類是否有父類,過載是否正確,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" 第一手更新,有興趣的朋友可以關注公眾號,第一時間看到筆者分享的各項知識點,謝謝!筆芯!

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