JVM - ClassLoader裝載流程

注意: 裝載加載 的區別:java

  • 裝載,指的是.class文件加載到初始化的整個生命週期;
  • 加載,指的是.class文件裝載的第一個階段

類裝載機制

jvm把class文件加載到內存中,並對數據進行校驗、解析和初始化,最終造成JVM能夠直接使用的java類的全過程。安全

ClassLoader的裝載流程圖

輸入圖片說明

類裝載的前提條件

class只有使用的時候纔會被裝載,java虛擬機也不會無條件的裝載class類型數據結構

裝載流程

  1. 加載類

加載類處於類裝載的第一個階段,將class文件的字節碼加載到內存中,並將靜態數據轉換成方法區中的運行時數據結構,在堆中生成一個表明這個類的java.lang.Class對象,做爲方法區類數據的訪問入口。jvm

該過程須要ClassLoader參與。佈局

加載類,JVM必須完成:測試

  • 經過類的全名,獲取類的二進制數據流
  • 解析類的二進制數據流爲方法區內的數據結構
  • 建立java.lang.Class類的實例,表示該類型
  1. 鏈接

將java類的二進制代碼合併到JVM的運行狀態中。這一步包含三個操做:優化

  • 驗證,確保加載的類信息符合JVM規範,沒有安全方面的問題
  • 準備 驗證經過後,虛擬機就會進入準備階段,在這個階段,虛擬機會爲這個類變量在方法區分配相應的內存空間,並設置初始值。下圖爲虛擬機爲各類類型變量默認的初始值:

輸入圖片說明

注意:線程

  • java並不支持boolean類型,對於boolean類型,內部實現是Int,因爲int的默認值是0,故對應的boolean默認值是false。code

  • 此處進行內存分配的只是類變量(static修飾的變量),不包括實例變量(實例變量會在對象實例化時隨着對象一塊兒分配在java堆中)對象

  • 解析,該階段的任務就是將類、接口、字段和方法的符號引用轉爲直接引用

符號應用,就是一些字面量的引用,和虛擬機的內部數據結構和內存佈局無關。

  1. 初始化類

初始化是類裝載的最後一個階段。若是前面的操做沒有問題,表示類能夠順利裝載到系統中。此時,類纔會開始執行java字節碼。

該階段的重要工做,是執行類的初始化方法<clinit>, 爲類變量賦予正確的值。方法<clinit>是由編譯器自動生成的,它是由類靜態成員的賦值語句以及static語句塊合併產生的。

在加載一個類以前,虛擬機老是會試圖加載該類的父類,所以父類的<clinit>方法老是在子類<clinit>以前被調用。

  1. 初始化一個類包含兩個步驟:
  • 若是類存在超類,先初始化超類
  • 若是類存在類初始化方法,就執行此方法
  1. 初始化一個接口只有一個步驟: 若是該接口存在接口初始化方法,就執行此方法,接口不初始化父接口。

注意

  • java編譯器並非爲全部的類都產生一個<clinit>初始化方法,如下幾種狀況就沒有: ① 類沒有申明類變量,也沒有任何靜態初始化語句(static代碼塊); ② 類申明瞭類變量,可是沒有任何的類變量初始化語句,也沒有靜態初始化語句進行初始化; ③ 類僅包含靜態final變量的類變量初始化語句,並且是編譯時候的常量
  • 初始化類的過程必須保持同步,若是有多個線程初始化一個類,僅僅容許一個線程執行初始化,其餘的線程都須要等待。。

類初始化

JVM規定:一個類或者接口在初次使用時,必須進行初始化。這裏的「使用」,指的是」主動使用」,包括如下幾種狀況:

  • 當建立一個類的實例時,好比使用new關鍵字,或者經過反射、克隆、反序列化
  • 當調用類的靜態方法時,即當使用了字節碼invoke static指令
  • 當使用類或接口的靜態字段時(final常量除外),即便用了getstatic或putstatic指令
  • 當使用java.lang.reflect包中的方法反射類的方法時
  • 當初始化子類時,必須先初始化父類
  • 做爲啓動虛擬機、含有main方法的那個類

除了以上狀況屬於主動使用外,其餘均屬於被動使用,被動使用不會引發類的初始化。

主動使用示例

public class Parent {
    static {
        System.out.println("Parent init.");
    }
}

public class Child extends Parent {
    static {
        System.out.println("Child init.");
    }
}

public class InitMain {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

上述示例聲明瞭三個類:Parent、Child(extends Parent)、InitMain。若Parent被初始化,static被執行將打印「Parent Init」;若Child被初始化,將會打印「Parent init」、"Child init"。執行InitMain,打印結果爲:

Parent init.
Child init.

總結:

根據上述示例可知,系統首先加載Parent類,接着裝載Child類。符合主動裝載中的兩個條件,使用new關鍵字建立類的實例會裝載相關的類,以及在初始化子類時,必選先初始化父類。

被動裝載示例

public class Parent {
    static {
        System.out.println("Parent init");
    }

    public static int v = 100;
}

public class Child extends Parent {
    static {
        System.out.println("Child init.");
    }
}

public class InitMain {
    public static void main(String[] args) {
        System.out.println(Child.v);
    } 
}

說明:Parent類中定義了類變量v,在InitMain測試類中,使用子類Child調用父類中的類變量v。

執行結果:

Parent init
100

總結:

在InitMain測試類中,經過子類Child直接訪問了Parent類中的static變量v,可是子類Child並未初始化,只有父類Parent完成初始化。因此,在引用一個字段時,只有直接定義該字段的類,纔會被初始化

注意:雖然子類Child沒有被初始化,可是此時Child類已經被系統加載,只是沒有進入到初始化階段

引用final常量

public class FinalFieldClass {
    public static final String CONST_STR = "hello world";
    
    static {
        System.out.println("FinalFieldClass init");
    }
}

public class FinalFieldTest {
    public static void main(String[] args) {
        System.out.println(FinalFieldClass.CONST_STR);
    }
}

運行結果: hello world.

分析:FinalFiledClass類沒有由於其常量字段CONST_STR被引用而初始化,這是由於在Class文件生成時,final常量因爲其不變性,作了適當的優化。

總結:

編譯後的FinalFieldClass.class中,並無引用FinalFieldClass類,而是將其final常量直接存放在常量池中,所以FinalFiledClass類天然不會被加載。javac在編譯時,將常量直接植入目標類,再也不使用被引用類。

注意:並非在代碼中出現的類,就必定會被加載或者初始化,若是不符合主動使用的條件,類就不會被初始化。

相關文章
相關標籤/搜索