JVM 之 (12) 類加載機制


1.概述

       Class文件由類裝載器裝載後,在JVM中將造成一份描述Class結構的元信息對象,經過該元信息對象能夠獲知Class的結構信息:如構造函數,屬性和方法等,Java容許用戶藉由這個Class相關的元信息對象間接調用Class對象的功能。java

      虛擬機把描述類的數據從class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。數據庫


2.工做機制

      類裝載器就是尋找類的字節碼文件,並構造出類在JVM內部表示的對象組件。在Java中,類裝載器把一個類裝入JVM中,要通過如下步驟:數組

     (1) 裝載:查找和導入Class文件;安全

     (2) 連接:把類的二進制數據合併到JRE中;網絡

        (a)校驗:檢查載入Class文件數據的正確性;數據結構

        (b)準備:給類的靜態變量分配存儲空間;多線程

        (c)解析:將符號引用轉成直接引用;ide

     (3) 初始化:對類的靜態變量,靜態代碼塊執行初始化操做


函數


    Java程序能夠動態擴展是由運行期動態加載和動態連接實現的;好比:若是編寫一個使用接口的應用程序,能夠等到運行時再指定其實際的實現(多態),解析過程有時候還能夠在初始化以後執行;好比:動態綁定(多態);

3.裝載

 在裝載階段,虛擬機須要完成如下3件事情
          (1) 經過一個類的全限定名來獲取定義此類的二進制字節流

        (2) 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構oop

        (3) 在Java堆中生成一個表明這個類的java.lang.Class對象,做爲方法區這些數據的訪問入口。(不一樣虛擬機機制不一樣,hotsport把Class對象放在方法區中)

    虛擬機規範中並無準確說明二進制字節流應該從哪裏獲取以及怎樣獲取,這裏能夠經過定義本身的類加載器去控制字節流的獲取方式,譬如:網絡、動態生成、數據庫等


4.驗證

    驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體會完成4個階段的檢驗動做:
                (1) 文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
                 (2) 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。
                 (3) 字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
                 (4) 符號引用驗證:確保解析動做能正確執行。
驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

5.準備

    準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量的定義爲:
public static int value=123;
        那變量value在準備階段事後的初始值爲0而不是123.由於這時候還沒有開始執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,因此把value賦值爲123的動做將在初始化階段纔會執行。
至於「特殊狀況」是指:public static final int value=123,即當類字段的字段屬性是ConstantValue時,會在準備階段初始化爲指定的值,因此標註爲final以後,value的值在準備階段初始化爲123而非0.

6.解析

        解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info

        符號引用與虛擬機實現的佈局無關,引用的目標並不必定要已經加載到內存中。各類虛擬機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須是一致的,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中。
        直接引用能夠是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。若是有了直接引用,那引用的目標一定已經在內存中存在。    


7.初始化

        初始化階段是類加載最後一個階段,前面的類加載階段以後,除了在加載階段能夠自定義類加載器之外,其它操做都由JVM主導。到了初始階段,纔開始真正執行類中定義的Java程序代碼。
        初始化階段是執行類構造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變量的賦值操做和靜態語句塊中的語句合併而成的。虛擬機會保證<client>方法執行以前,父類的<client>方法已經執行完畢。p.s: 若是一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器能夠不爲這個類生成<client>()方法。

    <clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。以下:
public class Test
{
    static
    {
        i=0;
        System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
    }
    static int i=1;
}
<clinit>()方法與實例構造器<init>()方法不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類<init>()方法執行以前,父類的<clinit>()方法方法已經執行完畢, 因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。以下:
public class Parent {
    
    public static int A = 1;
    
    static {
        A = 2;
    }
    
    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);     //2
    }
}
        <clinit>()方法對於類或者接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生產<clinit>()方法。
        接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法。但接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。
        虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確的加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有好事很長的操做,就可能形成多個線程阻塞,在實際應用中這種阻塞每每是隱藏的。
public class DealLoopTest
{
    static class DeadLoopClass
    {
        static
        {
            if(true)
            {
                System.out.println(Thread.currentThread()+"init DeadLoopClass");
                while(true)
                {
                }
            }
        }
    }
 
    public static void main(String[] args)
    {
        Runnable script = new Runnable(){
            public void run()
            {
                System.out.println(Thread.currentThread()+" start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread()+" run over");
            }
        };
 
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-0,5,main]init DeadLoopClass
須要注意的是,其餘線程雖然會被阻塞,但若是執行<clinit>()方法的那條線程退出<clinit>()方法後,其餘線程喚醒以後不會再次進入<clinit>()方法。同一個類加載器下,一個類型只會初始化一次。
將上面代碼中的靜態塊替換以下:
static
        {
            System.out.println(Thread.currentThread() + "init DeadLoopClass");
            try
            {
                TimeUnit.SECONDS.sleep(10);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
Thread[Thread-0,5,main] start
Thread[Thread-1,5,main] start
Thread[Thread-1,5,main]init DeadLoopClass (以後sleep 10s)
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over
虛擬機規範嚴格規定了有且只有5中狀況(jdk1.7)必須對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):

      (1) 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令的最多見的Java代碼場景是:使用new關鍵字實例化對象的時候,讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。

      (2) 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。

      (3) 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。

      (4) 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

      (5) 當使用jdk1.7動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行初始化,則須要先出觸發其初始化。

只有上述這五種狀況會觸發初始化,也稱爲對一個類進行主動引用,除此之外,全部其餘方式都不會觸發初始化,稱爲被動引用.

注意如下幾種狀況不會執行類初始化

  • 經過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 定義對象數組,不會觸發該類的初始化。
  • 常量在編譯期間會存入調用類的常量池中,本質上並無直接引用定義常量的類,不會觸發定義常量所在的類。
  • 經過類名獲取Class對象,不會觸發類的初始化。
  • 經過Class.forName加載指定類時,若是指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
  • 經過ClassLoader默認的loadClass方法,也不會觸發初始化動做。
  • final變量不會觸發此類的初始化,由於在編譯階段就存儲在常量池中.
相關文章
相關標籤/搜索