1. 程式人生 > >jvm載入class檔案的原理機制分析

jvm載入class檔案的原理機制分析

案例分析

A、B類中均包含靜態程式碼塊,非靜態程式碼塊以及構造器,A類是B類的父類。

public class A {

  static {
    System.out.print("A中靜態程式碼塊>>>");
  }

  {
    System.out.print("A中非靜態程式碼塊>>>");
  }

  public A() {
    System.out.print("A中構造器>>>");
  }
}
public class B extends A{

    static {
        System.out.print("B中靜態程式碼塊>>>"
); } { System.out.print("B中非靜態程式碼塊>>>"); } public B() { System.out.print("B中構造器>>>"); } }

那麼看看下面程式碼的執行結果。

public class ABTest {
  public static void main(String[] args) {
    A ab = new B();
    System.out.println("\n==========================\n"
); ab = new B(); } }

執行結果為:

A中靜態程式碼塊>>>B中靜態程式碼塊>>>A中非靜態程式碼塊>>>A中構造器>>>B中非靜態程式碼塊>>>B中構造器>>>
==========================

A中非靜態程式碼塊>>>A中構造器>>>B中非靜態程式碼塊>>>B中構造器>>>

總結:
1. 同一類中:靜態程式碼塊 => 非靜態程式碼塊 => 構造器
2. 父子類中:父類 => 子類;
3. 靜態程式碼塊只在第一次例項化(new)執行了,非靜態程式碼塊在每次例項化都執行。

看執行結果,上面的3條總結都沒問題,對於第3點,需要注意下:靜態程式碼塊其實不是跟著例項走的,而是跟著類走。看如下測試,通過Class.forName()動態載入類:

  public static void main(String[] args) throws ClassNotFoundException {
    Class.forName("B");
  }

執行結果:

A中靜態程式碼塊>>>B中靜態程式碼塊>>>

這裡並沒有執行例項化過程,但是靜態程式碼塊卻執行了,這也證明了靜態static程式碼塊並不是跟著例項走。下面將簡單介紹下類載入相關概念及過程,介紹完後再看看上面的例子,印象會更深刻。首先得了解下幾個比較重要的JVM的記憶體概念。

jvm的幾個重要記憶體概念

方法區

專門用來存放已經載入的類資訊、常量、靜態變數以及方法程式碼的記憶體區域。

常量池

是方法區的一部分,主要用來存放常量和類中的符號引用等資訊。

堆區

用於存放類的物件例項,如new、陣列物件。

棧區

由一個個棧幀組成的後進先出的結構,主要存放方法執行時產生的區域性變數、方法出口等資訊。

java類的生命週期

我們編寫完一個.java結尾的原始檔後,經過編譯後生成對應的一個或多個.class字尾結尾的檔案。該檔案也稱為位元組碼檔案,能在java虛擬機器中執行。而類的生命週期正是:從類(.class檔案)被載入到虛擬機器記憶體,到從記憶體中解除安裝為止。整個週期一共分為7個階段:

載入,驗證,準備,解析,初始化,使用,解除安裝

其中

  • 驗證,準備,解析統稱為連線
  • 載入,驗證,準備,初始化,解除安裝,這5個的順序是確定的。

值得注意的是,通常我們所說的類載入指的是:載入,驗證,準備,解析,初始化,這5個階段。

載入

該階段虛擬機器的任務主要是找到需要載入的類,並把類的資訊載入到jvm的方法區中,然後中例項化一個java.lang.Class物件,作為方法區中這個類的資訊的入口。

連線

連線階段有三個階段:驗證,準備,解析。主要任務是載入後的驗證工作以及一些初始化前的準備工作

驗證

當一個類被載入後,需要驗證下該類是否合法,以保證載入的類能在虛擬機器中正常執行。

準備

該階段主要是為類的靜態變數分配記憶體並設定為jvm預設的初始值;對於非靜態變數,則不會為它們分配記憶體。這裡靜態變數的初始值,不是由我們指定的,是jvm預設的。

  • 基本型別(int、long、short、char、byte、boolean、float、double)的預設值為0;
  • 引用型別預設值是null;
  • 常量的預設值為我們設定的值。比如我們定義final static int a = 1000,則在準備階段中a的初始值就是1000。

解析

這一階段的任務是把常量池中的符號引用轉換為直接引用,也就是具體的記憶體地址。在這一階段,jvm會將所有的類、介面名、欄位名、方法名等轉換為具體的記憶體地址。譬如:我們要在記憶體中找到一個類裡面的一個叫call的方法,顯然做不到,但是該階段,由於jvm已經將call這個名字轉換為指向方法區中的一塊記憶體地址了,也就是說我們通過call這個方法名會得到具體的記憶體地址,也就找到了call在記憶體中的位置了。

初始化

有且僅有 5種情況必須立即對類進行“初始化”

  1. 使用new關鍵字例項化物件、讀取或設定一個類的靜態欄位(被final修飾、已經在編譯器把結果放入常量池的靜態欄位除外),以及呼叫一個類的靜態方法的時候;
  2. 使用java.lang.reflect包的方法進行反射呼叫時,若類沒有進行初始化,需要先觸發其初始化;
  3. 當初始化一個類時,若其父類還沒有進行初始化,則需要先觸發其父類的初始化;
  4. 執行main方法,虛擬機器會先初始化其包含的那個主類
  5. 當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行初始化,則需要先觸發其初始化(這一點不是很懂)。

在類的初始化階段,只會初始化與類相關的靜態賦值語句和靜態語句,也就是有static關鍵字修飾的資訊,而沒有static修飾的賦值語句和執行語句在例項化物件的時候才會執行。(這正好解釋了案例中第3點結論)

使用

初始化階段的5種情況用了很強烈的限定詞:有且僅有,這5種行為稱為對一個類進行“主動引用”。其他所有引用類的方法(行為)都不會對類進行初始化,稱之為“被動引用”

《學習深入Java虛擬機器》一書中列舉了3個被動引用例子,我驗證了下,確實如此,不過還得到了新的啟發。這裡列出其中的2個例子,如下:

例子1:通過子類引用父類的靜態欄位,不會導致子類初始化

package classloading;

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

    public static int value = 123;
}
package classloading;

public class SubClass extends SuperClass{

    static {
        System.out.println("SubClass init!");
    }
}
package classloading;

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

執行結果:

SuperClass init!
123

結論:
通過子類SubClass來引用父類SuperClass的靜態欄位value,初始化的只是父類,並不會觸發子類的初始化。

例子2:常量在編譯階段會存入呼叫類的常量池中,不會觸發定義常量的類的初始化

package classloading;

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

    public static final String HELLO_WORLD = "hello world";
}
package classloading;

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO_WORLD);
    }
}

執行結果:

hello world

結論:
從列印的結果可以看到,並沒有初始化ConstClass類;但是從原始碼上看是引用了ConstClass類的常量。因為在NotInitialization類的編譯期中,通過常量優化,已經將常量 "hello world"儲存到了NotInitialization類的常量池中了。也就是說,NotInitialization中引用ConstClass.HELLO_WORLD其實是對自身常量池中常量引用

解除安裝

在使用完類後,需滿足下面,類將被解除安裝:
1. 該類所有的例項都已經被回收,也就是java隊中不存在該類的任何例項;
2. 載入該類的ClassLoader已經被回收了;
3. 該類對應的java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。

當上面三個條件都滿足後,jvm就會在方法區垃圾回收的時候對類進行解除安裝,類的解除安裝過程本質上就是在方法區中清空類資訊,結束整個類的生命週期。

jvm載入class檔案的原理機制

面試題中經常會問到JVM載入Class檔案的原理機制,結合上面的分析,引用下面網上的分析,更加容易理解。

        JVM中類的裝載是由類載入器(ClassLoader)和它的子類來實現的,Java中的類載入器是一個重要的Java執行時系統元件,它負責在執行時查詢和裝入類檔案中的類。
        由於Java的跨平臺性,經過編譯的Java源程式並不是一個可執行程式,而是一個或多個類檔案。當Java程式需要使用某個類時,JVM會確保這個類已經被載入、連線(驗證、準備和解析)和初始化。類的載入是指把類的.class檔案中的資料讀入到記憶體中,通常是建立一個位元組陣列讀入.class檔案,然後產生與所載入類對應的Class物件。載入完成後,Class物件還不完整,所以此時的類還不可用。當類被載入後就進入連線階段,這一階段包括驗證、準備(為靜態變數分配記憶體並設定預設的初始值)和解析(將符號引用替換為直接引用)三個步驟。最後JVM對類進行初始化,包括:  1)  如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;  2)  如果類中存在初始化語句,就依次執行這些初始化語句。
        類的載入是由類載入器完成的,類載入器包括:根載入器(BootStrap)、擴充套件載入器(Extension)、系統載入器(System)和使用者自定義類載入器(java.lang.ClassLoader的子類)。從Java 2(JDK 1.2)開始,類載入過程採取了父親委託機制(PDM)。PDM更好的保證了Java平臺的安全性,在該機制中,JVM自帶的Bootstrap是根載入器,其他的載入器都有且僅有一個父類載入器。類的載入首先請求父類載入器載入,父類載入器無能為力時才由其子類載入器自行載入。JVM不會向Java程式提供對Bootstrap的引用。

下面是關於幾個類載入器的說明:

  • Bootstrap:一般用原生代碼實現,負責載入JVM基礎核心類庫(rt.jar);
  • Extension:從java.ext.dirs系統屬性所指定的目錄中載入類庫,它的父載入器是Bootstrap;
  • System:又叫應用類載入器,其父類是Extension。它是應用最廣泛的類載入器。它從環境變數classpath或者系統屬性java.class.path所指定的目錄中記載類,是使用者自定義載入器的預設父載入器。