java虛擬機類加載機制(筆記)

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

類加載的時機

類加載的生命週期包含:加載、驗證、準備、解析、初始化、使用、卸載。其中,驗證、準備、解析3個部分稱爲鏈接。
在這裏插入圖片描述
虛擬機對於類的初始化階段嚴格規定了有且僅有隻有5種情況如果對類沒有進行過初始化,則必須對類進行“初始化”!

  • 遇到new(實例化對象)、getstatic、putstatic(讀取和設置類的靜態變量,被final修飾,以在編譯期把結果放到常量池的靜態字段除外)或者invokestatic(調用一個類的靜態方法)這4條指令是,如果類沒有初始化,則需要先觸發其初始化。
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果沒有初始化,需要初始化。
  • 當初始化一個類,如果父類沒有初始化,需要先觸發父類的初始化。
  • 虛擬機啓動的時候,用戶需要指定一個執行主類(main方法哪個類),虛擬機先初始化這個主類。
  • 如果一個java.lang.invoke.MethodHandle實例最後解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行初始化,則需要先觸發其初始化

除此之外,所有的引用類的方法都不會觸發初始化,稱爲被動引用。被動引用的例子如下:

  • 子類引用父類的靜態字段,不會導致子類的初始化
public class SuperClass {


    static {
        System.out.println(" supser class init");
    }
    public static int value = 123;
}
public class SubClass extends SuperClass {

    static {
        System.out.println(" subclass init ");
    }
}


public class NoInitialization1 {

    public static void main(String[] args) {

        System.out.println(SubClass.value);
    }
}

輸出結果:

supser class init
123

  • 通過數組定義來引用類,不會觸發此類的初始化
public class NoInitialization3 {

    public static void main(String[] args) {

        SuperClass[] superClasses = new SuperClass[3];
    }
}

沒有任何輸出
這段代碼觸發了一個[LSuperClass]的類的初始化階段,是由虛擬機自動生產的、直接繼承java.lang.object的子類,創建動作由字節碼指令newarray觸發。

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

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

    public static final String HELLOWORLD = "hello world";
}
public class NoInitialization2 {

    public static void main(String[] args) {

        System.out.println(ConstClass.HELLOWORLD);
    }
}

接口的加載過程和類不同,只有在:當一個類初始化的時候,要求其父類全部都已經初始化過了,但是一個接口在初始化時候,並不要求其父接口都完成初始化,只有真正使用父接口(如:引用接口定義的常量)的時候纔會初始化。

類加載的過程

類的加載過程也就是加載、驗證、準備、解析以及初始化。

加載

注意“加載”和“類加載”的區別。
在加載的階段,虛擬機只需要完成以下三件事:

  1. 通過類的全限定名來獲取定義此類的二進制字節流
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行數據結構
  3. 在內存中生成一個代表這個類的java.lang.class對象,作爲方法區這個類的接口。
    加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機自行定義,虛擬機規範沒有規定ci區域的具體數據的格式。
    加載階段和鏈接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段沒有開始,鏈接階段可能已經開始,但這些夾在加載階段中的動作,仍屬於鏈接階段的內容,這兩個階段的開始仍然保持着固定的先後次序。
驗證

驗證是鏈接的第一步,這一階段的目的是爲了保護class文件的字節流的信息符合虛擬機的要求,並且不會危害虛擬機自身的安全。驗證如果檢查不符合class文件的格式約束,虛擬機就應拋出一個java.lang.VerifyError異常或者子異常。驗證大概分爲4個驗證動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證

文件格式驗證

驗證字節流是否符合class文件格式規範,並且能被當前版本的虛擬機處理。這一階段可能驗證:

  1. 是否以魔數0xCAFFBABE開頭
  2. 主、次版本號是否在虛擬機處理範圍之內
  3. 常量池的常量中是否有不被支持的常量類型(檢查tag)
  4. 指向常量的各種索引值是否有指向不存在的常量或者不符合類型的常量
  5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數據
  6. Class文件各個部分及文件本身是否有被刪去的或者附件的其他信息

    這階段的驗證是基於二進制字節流進行的,只有通過這個階段的驗證,字節流纔會進入內存中的方法區中進行存儲,所以後面的階段全部是基於方法區的存儲進行的,不能直接操作字節流。
元數據驗證

對字節碼描敘的信息進行語義分析,保證其描敘信息符合java語言規範的要求。這些階段可能包含驗證點如下:

  1. 這個類是否有父類
  2. 這個類的父類是否不允許被繼承
  3. 如果這個類不是抽象類,是否實現了其父類或者接口之中要求實現的方法
  4. 類中的字段、方法是否與父類產生矛盾
字節碼驗證

這一階段主要目的是通過數據流和控制流分析,確定程序的語義是否合法,是否符合邏輯。這階段分析對類的方法體進行校驗分析,保證被校驗類的方法體在運行中不會作出危害虛擬機安全的事件:

  1. 保證任意時刻操作數棧與指令代碼序列都能夠配合工作
  2. 保證跳轉指令不會跳到方法體意外的字節碼指令上
  3. 保證方法體體中的類型轉換是有效的

在jdk1.6之後javac編譯器和java虛擬機,給方法體Code屬性的屬性表中增加了一項名爲“StackMapTable”的屬性,這描敘了方法體中所有基本塊開始本地變量表和操作棧應有的狀態,檢查StackMapTable屬性中的記錄是否合法即可。

符合引用驗證

最後一階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,發生在解析階段。符合引用驗證可以看做是對類自身意外的信息進行匹配性校驗,需要校驗以下內容:
1. 符合引用中通過字符串描述的全限定名是否能夠找到對應的類
2. 在指定類中是否存在符合方法的描述符以及簡單名稱所描述的方法和字段
3. 符合引用中的類、字段、方法的訪問屬性(private、protected、public、default)是否可被當前類訪問
……

準備

準備階段是正式爲類變量(static修飾的)分配內存並設置類變量初始化值的階段,這些變量所使用的內存都將在方法區中進行分配。這裏飛蛾複製通常是數據類型的零值。

  1. 類變量value,public static int value = 123;變量在準備階段過後的初始值爲0而不是123,把value設置成123的putstatic指令,存放在()方法中,把value賦值123,在初始化階段才執行。
  2. public static final int value = 123;如果類字段的字段屬性(ConstantValue)屬性,那麼準備階段的值爲屬性的值。

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
#####符號引用和直接引用
符號引用:符號引用是一組符合來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可,符號引用的字面量形式明確定義在java虛擬機規範的class文件格式中
直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。
解析動作主要主要針對類或者接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7種引用進行,分別對應CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info7種類型。

類或接口解析
  1. 如果該符號引用不是一個數組類型,那麼虛擬機將會把該符號代表的全限定名稱傳遞給調用這個符號引用的類。這個過程由於涉及驗證過程所以可能會觸發其他相關類的加載
  2. 如果該符號引用是一個數組類型,並且該數組的元素類型是對象。我們知道符號引用是存在方法區的常量池中的,該符號引用的描述符會類似”[java/lang/Integer”的形式,將會按照上面的規則進行加載,虛擬機將會生成一個代表此數組對象的直接引用如果該符號引用是一個數組類型,並且該數組的元素類型是對象。我們知道符號引用是存在方法區的常量池中的,該符號引用的描述符會類似”[java/lang/Integer”的形式,將會按照上面的規則進行加載,虛擬機將會生成一個代表此數組對象的直接引用
  3. 如果上面的步驟都沒有出現異常,那麼該符號引用已經在虛擬機中產生了一個直接引用,但是在解析完成之前需要對符號引用進行驗證,主要是確認當前調用這個符號引用的類是否具有訪問權限,如果沒有訪問權限將拋出java.lang.IllegalAccess異常如果上面的步驟都沒有出現異常,那麼該符號引用已經在虛擬機中產生了一個直接引用,但是在解析完成之前需要對符號引用進行驗證,主要是確認當前調用這個符號引用的類是否具有訪問權限,如果沒有訪問權限將拋出java.lang.IllegalAccess異常
字段解析

對字段的解析需要首先對其所屬的類進行解析,因爲字段是屬於類的,只有在正確解析得到其類的正確的直接引用才能繼續對字段的解析。對字段的解析主要包括以下幾個步驟:

  1. 如果該字段符號引用(後面簡稱符號)就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,解析結束
  2. 否則,如果在該符號的類實現了接口,將會按照繼承關係從下往上遞歸搜索各個接口和它的父接口,如果在接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,那麼久直接返回這個字段的直接引用,解析結束否則,如果在該符號的類實現了接口,將會按照繼承關係從下往上遞歸搜索各個接口和它的父接口,如果在接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,那麼久直接返回這個字段的直接引用,解析結束
  3. 否則,如果該符號所在的類不是Object類的話,將會按照繼承關係從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都相匹配的字段,那麼直接返回這個字段的直接引用,解析結束否則,如果該符號所在的類不是Object類的話,將會按照繼承關係從下往上遞歸搜索其父類,如果在父類中包含了簡單名稱和字段描述符都相匹配的字段,那麼直接返回這個字段的直接引用,解析結束
  4. 否則,解析失敗,拋出java.lang.NoSuchFieldError異常否則,解析失敗,拋出java.lang.NoSuchFieldError異常
    如果最終返回了這個字段的直接引用,就進行權限驗證,如果發現不具備對字段的訪問權限,將拋出java.lang.IllegalAccessError異常
    經過這些步驟後,對一個字段的解析就到此爲止,這裏有一個經典的問題:就是父類與子類構造方法調用的問題,比如下面這段代碼:
public class Test{
    public static void main(String[] args){
        new C();
    }
}

class A{
    public A(String name){
        System.out.println(name + " A constructor");
    }
}

class B{
    private A = new A("b");
    public B(){
        System.out.println("B constructor");
    }
}

class C extends B{
    private A = new A("c");
    public C(){
        System.out.println("C constructor");
    }
}

輸出:

b A constructor
B constructor
c A constructor
C constructor

通過這個例子以及字段解析的過程,我們可以更深刻理解爲什麼在具有繼承關係的類中,爲什麼總是先加載父類的構造方法以及初始化,然後才調用子類的構造方法以及初始化。

類方法的解析

進行類方法的解析仍然需要先解析此類方法的類,在正確解析之後需要進行如下的步驟:

  1. 類方法和接口方法的符號引用是分開的,所以如果在類方法表中發現class_index(類中方法的符號引用)的索引是一個接口,那麼會拋出java.lang.IncompatibleClassChangeError的異常
  2. 如果class_index的索引確實是一個類,那麼在該類中查找是否有簡單名稱和描述符都與目標字段相匹配的方法,如果有的話就返回這個方法的直接引用,查找結束如果class_index的索引確實是一個類,那麼在該類中查找是否有簡單名稱和描述符都與目標字段相匹配的方法,如果有的話就返回這個方法的直接引用,查找結束
  3. 否則,在該類的父類中遞歸查找是否具有簡單名稱和描述符都與目標字段相匹配的字段,如果有,則直接返回這個字段的直接引用,查找結束否則,在該類的父類中遞歸查找是否具有簡單名稱和描述符都與目標字段相匹配的字段,如果有,則直接返回這個字段的直接引用,查找結束
  4. 否則,在這個類的接口以及它的父接口中遞歸查找,如果找到的話就說明這個方法是一個抽象類,查找結束,返回java.lang.AbstractMethodError異常(因爲抽象類是沒有實現的)否則,在這個類的接口以及它的父接口中遞歸查找,如果找到的話就說明這個方法是一個抽象類,查找結束,返回java.lang.AbstractMethodError異常(因爲抽象類是沒有實現的)
  5. 否則,查找失敗,拋出java.lang.NoSuchMethodError異常否則,查找失敗,拋出java.lang.NoSuchMethodError異常
    如果最終返回了直接引用,還需要對該符號引用進行權限驗證,如果沒有訪問權限,就拋出java.lang.IllegalAccessError異常
接口方法的解析

同類方法解析一樣,也需要先解析出該方法的類或者接口的符號引用,如果解析成功,就進行下面的解析工作:

  1. 如果在接口方法表中發現class_index的索引是一個類而不是一個接口,那麼也會拋出java.lang.IncompatibleClassChangeError的異常
  2. 否則,在該接口方法的所屬的接口中查找是否具有簡單名稱和描述符都與目標字段相匹配的方法,如果有的話就直接返回這個方法的直接引用。查找結束否則,在該接口方法的所屬的接口中查找是否具有簡單名稱和描述符都與目標字段相匹配的方法,如果有的話就直接返回這個方法的直接引用。查找結束
  3. 否則,在該接口以及其父接口中查找,直到Object類,如果找到則直接返回這個方法的直接引用否則,在該接口以及其父接口中查找,直到Object類,如果找到則直接返回這個方法的直接引用
  4. 否則,查找失敗否則,查找失敗

初始化

到了初始化階段,虛擬機纔開始真正執行Java程序代碼,前文講到對類變量的初始化,但那是僅僅賦初值,用戶自定義的值還沒有賦給該變量。只有到了初始化階段,纔開始真正執行這個自定義的過程,所以也可以說初始化階段是執行類構造器方法的過程。那麼這個 方法是這麼生成的呢?

() 是編譯器自動收集類中所有類變量的賦值動作和靜態語句塊合併生成的。編譯器收集的順序是由語句在源文件中出現的順序決定的
() 方法與類的構造器方法不同,因爲前者不需要顯式調用父類構造器,因爲虛擬機會保證在子類的() 方法執行之前,父類的 方法已經執行完畢
由於父類的 方法會先執行,所以就表示父類的static方法會先於子類的 方法執行。這點也可以通過下面的代碼得到體現:

static class A{
    public static int a = 1;
    static{
        a = 2;
    }
}

static class B extends A{
    public static int b = a;
}

public static void main(String[] args){
    System.out.println(B.b);
}

得到的結果是2而不是1,這就驗證了父類的靜態方法會先於子類的static方法執行。

類加載器

類與類加載器

類加載器雖然用於實現類的加載,在java程序中起到的作用卻不止類的加載階段。對於任意一個類,都需要由加載器和這個類一同確立其在java虛擬機的唯一性,沒有類加載器都有唯一的空間。

package com.own.learn.jdk.cls1.classLoading;

import java.io.InputStream;

public class ClassLoadTest {


    public static void main(String[] args) throws Exception {

        final ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {

                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";

                    final InputStream is = this.getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }

                    final byte[] bytes = new byte[is.available()];
                    is.read(bytes);
                    return defineClass(name, bytes, 0, bytes.length);
                } catch (Exception e) {

                }
                return super.loadClass(name);
            }
        };

        final Object o = classLoader.loadClass("com.own.learn.jdk.cls1.classLoading.ClassLoadTest").newInstance();
        System.out.println(o.getClass());

        System.out.println(o instanceof com.own.learn.jdk.cls1.classLoading.ClassLoadTest);
    }
}

輸出:

class com.own.learn.jdk.cls1.classLoading.ClassLoadTest
false

雙親委派模型

從java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),由C++語言實現的,是虛擬機的一部分。另一種是所有其他類加載器,這些類加載器都由java語言的實現按,獨立虛擬機外部,並且全部繼承抽象類java.lang.classloader。類加載器分爲3種:

  1. 啓動類加載器(Bootstrap classloader):負責加載存放在<JAVA_HOME>\lib目錄中,或者被-Xbootclasspath參數所指定的路徑中,並按照虛擬機所識別的類庫加載到虛擬機內存中。
  2. 擴展類加載器(Extension classloader):這個類加載器由sun.misc.launcher$ExtClassloader實現按,負責<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴展類加載器。
  3. 應用程序類加載器(Application classloader):這個類加載器由sun.misc.launcher$AppClassLoader實現的。這個類加載器是classloader中的getSystemClassLoader()方法的返回值,也稱爲系統加載器,負責加載用戶路徑(classpath)上所指定的類庫。

在這裏插入圖片描述
類夾雜其之間的這種層次關係,稱爲類加載器的雙親委派模型。
雙親委派模型工作過程:如果一個類加載器收到類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個類加載器都是如此,因此所有的類記載器請求最終都應該傳送到頂層的啓動類加載器中,只有父加載器反饋自己無法完成這個加載請求時,子加載器纔會嘗試自己去加載。
好處

  1. Java類伴隨其類加載器具備了帶有優先級的層次關係,確保了在各種加載環境的加載順序。 比如我們要加載頂層的Java類——java.lang.Object類,無論我們用哪個類加載器去加載Object類,這個加載請求最終都會委託給Bootstrap ClassLoader,這樣就保證了所有加載器加載的Object類都是同一個類。如果沒有雙親委派模型,那就亂了套了,完全可以搞出Root::Object和L1::Object這樣兩個不同的Object類。
  2. 雙親委派模型對於java虛擬機的穩定很重要。先檢查類是否已經被架子啊過,如果沒有加載過則調用父類加載器的loadclass()方法如果父親類加載器爲空則默認使用啓動類加載器作爲父類加載器。如果父類加載器加載失敗,拋出classNotFoundException異常後,再調用自己的findclass()方法進行加載。(每一個類都只會被加載一次,避免了重複加載
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

Java自定義類加載器與雙親委派模型

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