虛擬機類加載機制

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

一. 類加載的時機

類的生命週期.png

    加載、驗證、準備、初始化和卸載這五個步驟的順序是確定的。

    類的加載過程必須按這個順序開始,而解析階段在某些時候可以在初始化之後開始,這是爲了支持Java語言的運行時綁定。

    何時進行類加載過程的第一階段:加載並沒有進行強制約束,單對於初始化階段,虛擬機規範則是嚴格規定了有且只有5中情況下必須立即對類進行“初始化”:

        (1)遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果沒有類進行過初始化,則需要先出發其初始化;

        (2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先出發其初始化;

        (3)當初始化一個類的時候,如果發現其父類還沒進行過初始化,則需先觸發其父類的初始化;

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

        (5)當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

        這5中場景中的行爲稱爲對一個類進行主動引用。

        除以上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! ");
}
}
/**
* 被動使用類字段演示三:
* 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
*/
public class ConstantClass {
static {
System.out.println("ConstantClass init! ");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization {
/**
* 非主動使用類字段演示
*/
public static void main(String[] args) {
//對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過其子類來引用父類中的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化
System.out.println(SubClass.value);
/**
* 被動使用類字段演示二:
*/
//通過數組定義來引用類,不會觸發此類的初始化
SuperClass[] sca = new SuperClass[10];
//非主動使用類字段演示
System.out.println(ConstantClass.HELLOWORLD);
}
}
運行結果:
SuperClass init!
123
hello world


    對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此通過其子類來引用父類中的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化

    通過數組定義來引用類,不會觸發此類的初始化;

    常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化


二. 類加載的過程

    1.加載

        加載是”類加載“過程的一個階段。

        在加載階段,虛擬機需要完成3件事:

            -- 通過一個類的權限定名來獲取定義此類的二進制字節流;

            --將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;

            --在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。


    2.驗證(類加載過程中非必須的)

        目的:確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

        1)文件格式驗證(基於二進制字節流的驗證)

            保證輸入的字節流能正確地解析並存儲於方法區之內,格式上符合描述一個java類型信息的要求。

            通過該階段的驗證後,字節流纔會進入內存的方法區中進行存儲。所以後面三個驗證階段都是基於方法區的存儲結構進行的,不會再直接操作字節流。

        2)元數據驗證(基於方法區的存儲結構)

            對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求。即對元數據信息中的數據類型做檢驗。

        3)字節碼驗證

            通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

            對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件。

        4)符號引用驗證(連接的第三階段--解析階段)

            發生在虛擬機將符號引用轉換爲直接引用的時候,這個轉化動作將在連接的第三階段--解析階段中發生。

            可看作是對類自身以外(常量池中的各種符號引用)的信息進行匹配校驗。


    3.準備

        正式爲類變量(static)分配內存並設置類變量初始值(零值)的階段。這些變量所使用的內存都將在方法區中進行分配。


    4.解析

        虛擬機將常量池內的符號引用替換爲直接引用的過程。

        符號引用:以一組符號來描述所引用的目標。

                        符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中。

        直接引用:可以是直接指向目標的指針、相對偏移量或是一個能直接定位到目標的句柄。

                        直接引用時和虛擬機實現的內存佈局相關的。如果有了直接引用,那引用的目標必定已經在內存中存在。

        類或接口解析、字段解析、類方法解析、接口方法解析


    5.初始化

        初始化時類加載過程的最後一步。初始化階段才真正開始執行類中定義的Java程序代碼(字節碼)。

        初始化階段是執行類構造器<clinit>()方法的過程。

        <clinit>()方法:由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的。編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊只能訪問到定義在靜態語句塊前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但不能訪問。

        eg:非法向前引用

        image.png

        <clinit>()方法與類的構造函數(<init>())不同,它不需要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。


三. 類加載器

    通過一個類的全限定名來獲取描述此類的二進制字節流。實現這個動作的代碼塊稱爲類加載器。

    每個類加載器都擁有一個獨立的類名稱空間。比較兩個類是否“相等(equals()、isAssignableFrom()、isInstance()、instanceof)”,只有在這兩個類是由同一個類加載器加載的前提下才有意義。

package jvm.classloader;
/**
* Created by Dong zhuying on 2018/6/6.
*/
import java.io.IOException;
import java.io.InputStream;
/**
* 類加載器與instanceof關鍵字演示
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
//自定義的類加載器加載
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is==null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};

Object obj = myLoader.loadClass("jvm.classloader.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof jvm.classloader.ClassLoaderTest);
}
}
/**
* 運行結果:
* class jvm.classloader.ClassLoaderTest
* false
*/





四. 虛擬機字節碼執行引擎

    1.運行時棧幀結構

        棧幀:用於支持虛擬機進行方法調用和方法執行的數據結構。是虛擬機運行時數據區中的虛擬機棧的棧元素。

        棧幀儲存了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章