天天看點

深入了解Java虛拟機(六)之類加載機制

深入了解Java虛拟機系列文章

  • 深入了解Java虛拟機(一)之記憶體布局和對象的建立
  • 深入了解Java虛拟機(二)之四種引用和2次标記
  • 深入了解Java虛拟機(三)之垃圾收集
  • 深入了解Java虛拟機(四)之JVM調優
  • 深入了解Java虛拟機(五)之Class類檔案結構
  • 深入了解Java虛拟機(七)之Java記憶體模型
類加載機制:虛拟機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型
  • Java語言裡面,類型的加載、連接配接和初始化過程都是在程式運作期間完成的,Java的動态可擴充特性就是依賴運作期動态加載和動态連接配接實作的

類加載時機

  • 類從被加載到虛拟機,到解除安裝出記憶體,包括:加載、驗證、準備、解析、初始化、使用和解除安裝這幾個過程。其中,驗證、準備和解析統稱為連接配接
  • 加載、驗證、準備、初始化和解除安裝這5個階段的順序是确定的
虛拟機規範規定有且隻有5種情況必須立即對類進行初始化(而加載、驗證、準備自然需要在此之前開始)
  • 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:
    • 使用new關鍵字執行個體化對象的時候
    • 讀取或設定一個類的靜态字段(被final修飾、已在編譯期把結果放入常量池的靜态字段除外)的時候
    • 調用一個類的靜态方法的時候
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則先要觸發其初始化
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
  • 當虛拟機啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛拟機會先初始化這個主類
  • 當使用JDK1.7的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getstatic、REF_putstatic、REF_invokeStatic的方法句柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化
  • 以上5種場景的行為稱為對一個類進行主動引用
幾個對類的被動引用(被動引用不會觸發初始化)
  • 對于靜态字段,隻有直接定義這個字段的類才會被初始化,是以通過子類來引用父類中定義的靜态字段,隻會觸發父類的初始化而不會觸發子類的初始化
//父類
public class SuperClass{
    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;

}

//子類
public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

//隻會輸出“SuperClass init!”,而不會輸出“SubClass init!”
//通過子類引用父類的靜态字段,不會導緻子類初始化。
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}
           
  • 通過數組定義來引用類,不會觸發該類的初始化
//将上面的NotInitialization中的main方法裡面稍作修改
//不會輸出“SuperClass init!”,因為沒有觸發SuperClass的初始化
public class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}
           
  • 常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,是以不會觸發定義常量的類的初始化
public class ConstClass{
    static {
        System.out.println("ConstClass init!");
    }
    
    public static final String HELLOWORLD = "hello world";
}

//不會輸出“ConstClass init!”
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}
           
接口的加載的特殊性
  • 一個接口在初始化時,并不要求其父接口全部都完成了初始化,隻有在真正使用到父接口的時候(如引用接口定義的常量)才會初始化

類加載的過程

加載
  • 加載過程,虛拟機需要完成3件事:
    • 通過一個類的全限定名來擷取定義此類的二進制位元組流
    • 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構
    • 在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種資料的通路入口
  • 數組類本身不通過類加載器加載,它是由Java虛拟機直接建立的,而數組類的元素類型最終是靠類加載器去建立的
  • 加載階段與連接配接階段的部分内容(如一部分位元組碼檔案格式驗證動作)是交叉進行的
驗證
  • 驗證階段大緻可分為4個階段:檔案格式驗證、中繼資料驗證、位元組碼驗證、符号引用驗證
  • 檔案格式驗證主要驗證位元組流是否符合Class檔案格式的規範,并且能被目前版本的虛拟機處理。隻有通過了這個階段的驗證後,位元組流才會進入記憶體的方法區中進行存儲
  • 中繼資料驗證是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求
  • 位元組碼驗證主要是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的,這個階段将對類的方法體進行校驗分析
  • 符号引用驗證可以看作是對類自身以外(常量池中的各種符号引用)的資訊進行比對性校驗,目的是確定解析動作能正常執行
準備
  • 準備階段是正式為類變量(static修飾的變量)配置設定記憶體并設定初始值的階段,這些類變量所使用的記憶體都将在方法區中進行配置設定
  • 初始值通常都是資料類型的零值,比如int型就是0,boolean型就是false等。而給類變量指派程式員設定的初始值,則要到初始化階段
//在準備階段,value會被指派為初始值0;到了初始化階段才會指派為123
//因為把value指派為123的putstatic指令是程式編譯後存放于類構造器<clinit>方法中的
public static int value = 123;
           
  • 如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化為ConstantValue屬性所指的值
//在準備階段,value會被指派為初始值123,因為編譯時Javac會為value生成ConstantValue屬性
public static final int value = 123;
           
解析
  • 解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程
  • 符号引用和直接引用
    • 符号引用:以一組符号來描述所引用的目标,可以使任何形式的字面量,隻要使用時能無歧議地定位到目标即可。與虛拟機的記憶體布局無關,引用的目标不一定已經加載到記憶體中。各個虛拟機能接受的符号引用必須一緻
    • 直接引用:可以是直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。和虛拟機實作的記憶體布局有關。如果有了直接引用,那引用的目标必定已經在記憶體中存在
  • 虛拟機規定在執行16個用于操作符号引用的位元組碼指令之前,先對它們所使用的符号引用進行解析:anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic
  • 除了invokedynamic指令外,虛拟機實作可以通過在運作時常量池中直接記錄直接引用,并把常量标示為已解析狀态,來對第一次解析的結果進行緩存
  • 在同一個實體中,如果一個符号引用之前被成功解析過,那後面的引用解析就應當一直成功;反之也是
  • 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符
  • 在字段解析中,如果有一個同名字段同時出現在現在類或接口的接口和父類中,或者同時在自己或父類的多個接口中出現,那編譯器将可能拒絕編譯
初始化
  • 初始化階段真正開始執行類中定義的Java程式代碼。初始化階段是執行類構造器()方法的過程
  • ()方法是由編譯器自動收集類中所有類變量的指派動作和靜态語句塊(static{}塊)中的語句合并産生的,收集的順序由語句在源檔案中出現的順序決定。靜态語句塊中隻能通路到定義在靜态語句塊之前的變量,可以指派但不能通路後面定義的變量。
public class Test {
    static {
        i = 0;  //給變量指派可以正常編譯通過
        System.out.print(i);    //這句編譯器會提示“非法向前引用”
    }
    static int i = 1;
}
           
  • ()方法不需要顯示地調用父類構造器,虛拟機會保證在子類的()方法執行之前,父類的()方法已經執行完畢。是以,虛拟機中第一個被執行的()方法的類肯定是java.lang.Object
  • 父類中定義的靜态語句塊要優先于子類的變量指派操作
  • ()方法對于類或者接口來說并不是必需的
  • 接口中不能使用靜态語句塊,但仍然有變量初始化的指派操作,是以也會生成()方法。執行接口的()方法不需要先執行父接口的()方法。隻有當父接口中定義的變量使用時,父接口才會初始化。接口的實作類在初始化時也一樣不會執行接口的()方法。
  • 虛拟機會保證一個類的()方法在多線程環境中被正确地加鎖、同步,同一時間隻會有一個線程去執行類的()方法。同一個類加載器下,一個類型隻會初始化一次,即()方法隻會執行一次。

類加載器

  • 對于任意一個類,都需要由加載它的類加載器和這個類本身一同确立其在虛拟機中的唯一性。比較2個類是否“相等”隻有在這2個類是在同一個類加載器加載的前提下才有意義。這裡所指的“相等”包括:equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof等判定情況
  • 系統提供的類加載器包括以下3種:啟動類加載器<–擴充類加載器<–應用程式類加載器<–自定義的類加載器
  • 類加載器之間遵循雙親委派模型的層次關系,除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器。其中的父子關系都是使用組合關系來實作,通過組合關系複用父加載器的代碼
  • 雙親委派模型的好處是Java類型随着它的類加載器一起具備了一種帶優先級的層次關系。比如無論哪個類加載器加載java.lang.Object類,最終都是委派給模型頂端的啟動類加載器進行加載,是以Object類在程式的各種類加載器環境中都是同一個類。

歡迎關注我的微信公衆号,和我一起學習一起成長!

深入了解Java虛拟機(六)之類加載機制