深入理解JVM——(四)類載入機制

PAce發表於2019-01-21

一、什麼是類的載入

類的載入是將該類.class二進位制資料讀取到記憶體中,將其資料放在方法區內,然後再中建立一個java.lang.Class物件,用來封裝方法區內的資料結構。

類的載入的最終產品是位於堆中的class物件,class物件封裝了類在方法區資料結構,併為程式設計師提供訪問方法區資料結構的介面。

img

類載入器不需要等到某個類“被首次使用”時才載入,JVM允許載入器預料某個類將被使用時就預先載入它。

如果類載入過程中遇到了.class檔案的錯誤或缺失,類載入器只有首期呼叫此類時才會報錯,如果一直沒有主動使用,那麼便不會報錯。

二、類的生命週期

img

類的載入過程包含五個階段:載入、驗證、準備、解析、初始化

五階段中載入,驗證,準備,初始化四個階段的開始順序固定,解析階段在某些情況下會在初始化之後開始,這是為了支援Java的動態繫結。需要注意的是,這裡說的按順序開始並不是按順序執行或結束,這些階段是交叉混合執行的。

2.1 載入

載入階段主要進行查詢並載入類的二進位制資料,需要完成以下三件事情:

  1. 通過一個類的全限定類名獲取此類的二進位制位元組流
  2. 將位元組流表示的靜態儲存結構轉化為方法區的執行時儲存結構
  3. 在記憶體中生成class物件,並做為方法區資料型別的入口

對於類載入的其他階段,載入階段是可控性最強的階段,即開發人員既可以使用系統的類載入器載入,也可以使用自定義的載入器載入。

2.2 連線

2.2.1 驗證

驗證階段主要確保被載入的類正確性

驗證是連線階段的第一步,目的是確保Class檔案中的位元組流包含的資訊符合虛擬機器的要求,並不會危害虛擬機器本身。驗證階段大致完成4個校驗動作:

  • 檔案格式校驗:判斷位元組流是否符合Class檔案格式
  • 後設資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外
  • 位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的
  • 符號引用驗證:確保解析動作能正確執行

驗證階段非常重要,但不是必須的,它對程式執行沒有影響,可以考慮採用-Xverifynone引數來關閉大部分的類驗證證措施,以縮短虛擬機器類載入的時間。

2.2.2 準備

準備階段為類的靜態變數分配記憶體,並初始化預設值

準備階段是正式為類變數分配記憶體並設定變數初始值的階段,這些記憶體都將在方法區中分配。對於該階段有以下幾點需要注意:

  1. 這時候進行記憶體分配的僅包括類變數(被static修飾的變數),使用的方法區中的記憶體。不包括例項變數,例項變數會在物件例項化時隨著物件分配在java堆中

  2. 初始值一般為 0 值或null,例如下面的類變數 value 被初始化為 0 而不是 123。

    public static int value = 123;
    複製程式碼

    這時候尚未開始執行任何java方法,把value賦值為123的動作將在初始化階段才會被執行。

  3. 如果類變數是常量(被static final修飾),那麼會按照表示式來進行初始化,而不是賦值為 0。

    public static final int value = 123
    複製程式碼

2.2.3 解析

解析階段是將常量池中符號引用替換為直接引用的過程。

  • 符號引用:符號引用指用一組符號來描述所引用的目標,符號可以是約定好的任何形式字面量,引用的目標不一定載入到記憶體中
  • 直接引用:直接引用可以是直接指向目標的指標、相對偏移量或一個能間接定位到目標的控制程式碼。引用的目標一點在記憶體中。

2.3 初始化

初始化階段才真正開始執行類中定義的 Java 程式程式碼。初始化階段即虛擬機器執行類構造器 () 方法的過程。在準備階段,類變數已經賦過一次系統要求的初始值,而在初始化階段,根據程式設計師通過程式制定的主觀計劃去初始化類變數和其它資源。

JVM初始化步驟

① 假如這個類還沒有被載入和連線,則程式先載入並連線該類。

② 假如該類的直接父類還沒有被初始化,則先初始化其直接父類。

③ 假如類中有初始化語句,則系統依次執行這些初始化語句。

類初始化時機:只有當對類主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:

  • 建立類的例項,也就是new的方式
  • 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
  • 呼叫一個類的靜態方法
  • 反射(如Class.forName(“com.shengsiyuan.Test”)
  • 初始化某個類的子類,則其父類也會被初始化
  • Java虛擬機器啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來執行某個主類

以上六種情況稱為主動使用,其他的情況均稱為被動使用,被動使用不會導致初始化。

主動引用示例

對於類而言,初始化子類會導致父類(不包括介面)的初始化

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static final int value = 123;
}

class SubClass extends SuperClass {
    public static int i = 3;
    static {
        System.out.println("sub");
    }
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SubClass.i);
    }
}
複製程式碼

執行結果:

super
sub
3
複製程式碼

說明:初始化子類會導致父類的初始化,並且父類的初始化在子類初始化的前面。

被動引用例項

① 子類引用父類靜態欄位(非final),不會導致子類初始化

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("sub");
    }
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}
複製程式碼

執行結果:

super
123
複製程式碼

說明:並沒有初始化子類,雖然使用SubClass.value,但實際使用的是子類繼承父類的靜態欄位,不會初始化SubClass。即只有直接定義了這個欄位的類才會被初始化。

② 對於類或介面而言,使用其常量欄位(final、static)不會導致其初始化

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static final int value = 123;
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SuperClass.value);
    }
}
複製程式碼

執行結果:

123
複製程式碼

說明:類或介面的常量並不會導致類或介面的初始化。因為常量在編譯時進行優化,直接嵌入在TestInit.class檔案的位元組碼中。

③ 通過陣列定義引用類,不會觸發此類的初始化

package com.demo;
 
class SuperClass1{
    
    public static int value = 123;
    
    static
    {
        System.out.println("SuperClass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        SuperClass1[] scs = new SuperClass1[10];
    }
}
複製程式碼

執行結果為空

對於介面而言,初始化子介面不會導致父介面的初始化,只有在真正使用到父介面的時候(如使用父介面中定義的常量),才會初始化

2.4 結束生命週期

在如下幾種情況下,Java虛擬機器將結束生命週期

  • 執行了System.exit()方法
  • 程式正常執行結束
  • 程式在執行過程中遇到了異常或錯誤而異常終止
  • 由於作業系統出現錯誤而導致Java虛擬機器程式終止

三、類載入器

尋找類載入器,先來一個小例子

package com.neo.classloader;
public class ClassLoaderTest {
     public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}
複製程式碼

執行後,輸出結果:

sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null
複製程式碼

從上面的結果可以看出,並沒有獲取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引導類載入器)是用C語言實現的,找不到一個確定的返回父Loader的方式,於是就返回null。

這幾種類載入器的層次關係如下圖所示:

img

注意:這裡父類載入器並不是通過繼承關係來實現的,而是採用組合實現的。

從 Java 虛擬機器的角度來講,只存在以下兩種不同的類載入器:

  • 啟動類載入器:這個類載入器用 C++ 實現,是虛擬機器自身的一部分。
  • 所有其他類的載入器:這些類由 Java 實現,獨立於虛擬機器外部,並且全都繼承自抽象java.lang.ClassLoader

從 Java 開發人員的角度看,類載入器可以劃分得更細緻一些:

  • 啟動類載入器Bootstrap ClassLoader,負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.開頭的類均被Bootstrap ClassLoader載入)。啟動類載入器是無法被Java程式直接引用的
  • 擴充套件類載入器Extension ClassLoader,該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴充套件類載入器。
  • 應用程式類載入器Application ClassLoader,該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

除此之外,我們還可以加入自定義的類載入器,可以實現以下功能:

  1. 在執行非置信程式碼之前,自動驗證數字簽名。
  2. 動態地建立符合使用者特定需要的定製化構建類。
  3. 從特定的場所取得java class,例如資料庫中和網路中。

JVM類載入機制

  • 全盤負責:當一個類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入
  • 父類委託:先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類
  • 快取機制:快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區尋找該Class,只有快取區不存在,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入快取區。這就是為什麼修改了Class後,必須重啟JVM,程式的修改才會生效

四、類的載入

類載入有三種方式:

  • 命令列啟動應用時候由JVM初始化載入
  • 通過Class.forName()方法動態載入
  • 通過ClassLoader.loadClass()方法動態載入

例子:

package com.neo.classloader;
public class loaderTest { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()來載入類,不會執行初始化塊 
                loader.loadClass("Test2"); 
                //使用Class.forName()來載入類,預設會執行初始化塊 
                //Class.forName("Test2"); 
                //使用Class.forName()來載入類,並指定ClassLoader,初始化時不執行靜態塊 
                //Class.forName("Test2", false, loader); 
        } 
}
複製程式碼

demo類

public class Test2 { 
        static { 
                System.out.println("靜態初始化塊執行了!"); 
        } 
}
複製程式碼

分別切換載入方式,會有不同的輸出結果。

Class.forName()和ClassLoader.loadClass()區別

  • Class.forName():將類的.class檔案載入到jvm中之外,還會對類進行解釋,執行類中的static塊.
  • ClassLoader.loadClass():只幹一件事情,就是將.class檔案載入到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。
  • Class.forName(name, initialize, loader)帶參函式也可控制是否載入static塊。並且只有呼叫了newInstance()方法採用呼叫建構函式,建立類的物件 。

五、雙親委派模型

img

上圖即雙親委派模型

5.1 雙親委派機制:

  1. AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
  2. ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入.
  4. ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException

5.2 好處

使得 Java 類隨著它的類載入器一起具有一種帶有優先順序的層次關係,從而使得基礎類得到統一。

  • 系統類防止記憶體中出現多份同樣的位元組碼
  • 保證Java程式安全穩定執行

5.3 原始碼實現

ClassLoader原始碼分析:

public Class<?> loadClass(String name)throws ClassNotFoundException {
        return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
        // 首先判斷該型別是否已經被載入
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果沒有被載入,就委託給父類載入或者委派給啟動類載入器載入
            try {
                if (parent != null) {
                     //如果存在父類載入器,就委派給父類載入器載入
                    c = parent.loadClass(name, false);
                } else {
                //如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類,通過呼叫本地方法native Class findBootstrapClass(String name)
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
             // 如果父類載入器和啟動類載入器都不能完成載入任務,才呼叫自身的載入功能
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
複製程式碼

參考:

jvm系列(一):java類的載入機制

Java 虛擬機器

深入理解虛擬機器之虛擬機器類載入機制

相關文章