一道面試題搞懂JVM類載入機制

TuxedoLinux發表於2018-05-22
JVM(四)—一道面試題搞懂JVM類載入機制

有這樣一道面試題:

class Singleton{
    private static Singleton singleton = new Singleton();
    public static int value1;
    public static int value2 = 0;

    private Singleton(){
        value1++;
        value2++;
    }

    public static Singleton getInstance(){
        return singleton;
    }

}

class Singleton2{
    public static int value1;
    public static int value2 = 0;
    private static Singleton2 singleton2 = new Singleton2();

    private Singleton2(){
        value1++;
        value2++;
    }

    public static Singleton2 getInstance2(){
        return singleton2;
    }

}

public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("Singleton1 value1:" + singleton.value1);
        System.out.println("Singleton1 value2:" + singleton.value2);

        Singleton2 singleton2 = Singleton2.getInstance2();
        System.out.println("Singleton2 value1:" + singleton2.value1);
        System.out.println("Singleton2 value2:" + singleton2.value2);
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

說出執行的結果:
Singleton1 value1 : 1
Singleton1 value2 : 0
Singleton2 value1 : 1
Singleton2 value2 : 1

稍後會帶來分析。

一 類載入機制

JVM類載入分為5個過程:載入,驗證,準備,解析,初始化,使用,解除安裝,如下圖所示:
這裡寫圖片描述

下面來看看載入,驗證,準備,解析,初始化這5個過程的具體動作。

1.1 載入

載入主要是將.class檔案(並不一定是.class。可以是ZIP包,網路中獲取)中的二進位制位元組流讀入到JVM中。
在載入階段,JVM需要完成3件事:
1)通過類的全限定名獲取該類的二進位制位元組流;
2)將位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
3)在記憶體中生成一個該類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

1.2 連線

1.2.1 驗證

驗證是連線階段的第一步,主要確保載入進來的位元組流符合JVM規範。
驗證階段會完成以下4個階段的檢驗動作:
1)檔案格式驗證
2)後設資料驗證(是否符合Java語言規範)
3)位元組碼驗證(確定程式語義合法,符合邏輯)
4)符號引用驗證(確保下一步的解析能正常執行)

1.2.2 準備

準備是連線階段的第二步,主要為靜態變數在方法區分配記憶體,並設定預設初始值。

1.2.3 解析

解析是連線階段的第三步,是虛擬機器將常量池內的符號引用替換為直接引用的過程。

1.3 初始化

初始化階段是類載入過程的最後一步,主要是根據程式中的賦值語句主動為類變數賦值。
注:
1)當有父類且父類為初始化的時候,先去初始化父類;
2)再進行子類初始化語句。

什麼時候需要對類進行初始化?
1)使用new該類例項化物件的時候;
2)讀取或設定類靜態欄位的時候(但被final修飾的欄位,在編譯器時就被放入常量池的靜態欄位除外static final);
3)呼叫類靜態方法的時候;
4)使用反射Class.forName(“xxxx”)對類進行反射呼叫的時候,該類需要初始化;
5) 初始化一個類的時候,有父類,先初始化父類(注:1. 介面除外,父介面在呼叫的時候才會被初始化;2.子類引用父類靜態欄位,只會引發父類初始化);
6) 被標明為啟動類的類(即包含main()方法的類)要初始化;
7)當使用JDK1.7的動態語言支援時,如果一個java.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

以上情況稱為對一個類進行主動引用,且有且只要以上幾種情況需要對類進行初始化。

再回過頭來分析一開始的面試題:
Singleton輸出結果:1 0
原因:

1 首先執行main中的Singleton singleton = Singleton.getInstance();
2 類的載入:載入類Singleton
3 類的驗證
4 類的準備:為靜態變數分配記憶體,設定預設值。這裡為singleton(引用型別)設定為null,value1,value2(基本資料型別)設定預設值0
5 類的初始化(按照賦值語句進行修改):
執行private static Singleton singleton = new Singleton();
執行Singleton的構造器:value1++;value2++; 此時value1,value2均等於1
執行
public static int value1;
public static int value2 = 0;
此時value1=1,value2=0

Singleton2輸出結果:1 1
原因:

1 首先執行main中的Singleton2 singleton2 = Singleton2.getInstance2();
2 類的載入:載入類Singleton2
3 類的驗證
4 類的準備:為靜態變數分配記憶體,設定預設值。這裡為value1,value2(基本資料型別)設定預設值0,singleton2(引用型別)設定為null,
5 類的初始化(按照賦值語句進行修改):
執行
public static int value2 = 0;
此時value2=0(value1不變,依然是0);
執行
private static Singleton singleton = new Singleton();
執行Singleton2的構造器:value1++;value2++;
此時value1,value2均等於1,即為最後結果


二 類載入器

類載入器實現的功能是即為載入階段獲取二進位制位元組流的時候。

JVM提供了以下3種系統的類載入器:

  • 啟動類載入器(Bootstrap ClassLoader):最頂層的類載入器,負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類。
  • 擴充套件類載入器(Extension ClassLoader):負責載入 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。
  • 應用程式類載入器(Application ClassLoader):也叫做系統類載入器,可以通過getSystemClassLoader()獲取,負責載入使用者路徑(classpath)上的類庫。如果沒有自定義類載入器,一般這個就是預設的類載入器。

類載入器之間的層次關係如下:
這裡寫圖片描述
照片來源:http://www.importnew.com/25295.html

類載入器之間的這種層次關係叫做雙親委派模型。
雙親委派模型要求除了頂層的啟動類載入器(Bootstrap ClassLoader)外,其餘的類載入器都應當有自己的父類載入器。這裡的類載入器之間的父子關係一般不是以繼承關係實現的,而是用組合實現的。

雙親委派模型的工作過程

如果一個類接受到類載入請求,他自己不會去載入這個請求,而是將這個類載入請求委派給父類載入器,這樣一層一層傳送,直到到達啟動類載入器(Bootstrap ClassLoader)。
只有當父類載入器無法載入這個請求時,子載入器才會嘗試自己去載入。

雙親委派模型的程式碼實現

雙親委派模型的程式碼實現集中在java.lang.ClassLoader的loadClass()方法當中。
1)首先檢查類是否被載入,沒有則呼叫父類載入器的loadClass()方法;
2)若父類載入器為空,則預設使用啟動類載入器作為父載入器;
3)若父類載入失敗,丟擲ClassNotFoundException 異常後,再呼叫自己的findClass() 方法。

loadClass原始碼如下:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先檢查類是否被載入
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 沒有則呼叫父類載入器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父類載入器為空,則預設使用啟動類載入器作為父載入器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父類載入失敗,丟擲ClassNotFoundException 異常後
            c = findClass(name);
        }
    }
    if (resolve) {
        //5 再呼叫自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

破壞雙親委派模型

雙親委派模型很好的解決了各個類載入器載入基礎類的統一性問題。即越基礎的類由越上層的載入器進行載入。
若載入的基礎類中需要回撥使用者程式碼,而這時頂層的類載入器無法識別這些使用者程式碼,怎麼辦呢?這時就需要破壞雙親委派模型了。
下面介紹兩個例子來講解破壞雙親委派模型的過程。

  1. JNDI破壞雙親委派模型
    JNDI是Java標準服務,它的程式碼由啟動類載入器去載入。但是JNDI需要回撥獨立廠商實現的程式碼,而類載入器無法識別這些回撥程式碼(SPI)。
    為了解決這個問題,引入了一個執行緒上下文類載入器。 可通過Thread.setContextClassLoader()設定。
    利用執行緒上下文類載入器去載入所需要的SPI程式碼,即父類載入器請求子類載入器去完成類載入的過程,而破壞了雙親委派模型。

  2. Spring破壞雙親委派模型
    Spring要對使用者程式進行組織和管理,而使用者程式一般放在WEB-INF目錄下,由WebAppClassLoader類載入器載入,而Spring由Common類載入器或Shared類載入器載入。
    那麼Spring是如何訪問WEB-INF下的使用者程式呢?
    使用執行緒上下文類載入器。 Spring載入類所用的classLoader都是通過Thread.currentThread().getContextClassLoader()獲取的。當執行緒建立時會預設建立一個AppClassLoader類載入器(對應Tomcat中的WebAppclassLoader類載入器): setContextClassLoader(AppClassLoader)。
    利用這個來載入使用者程式。即任何一個執行緒都可通過getContextClassLoader()獲取到WebAppclassLoader。

三 附上Tomcat類載入架構:

這裡寫圖片描述
(圖片來源:http://lib.csdn.net/article/java/60356
Tomcat目錄下有4組目錄:

  • /common目錄下:類庫可以被Tomcat和Web應用程式共同使用;由 Common ClassLoader類載入器載入目錄下的類庫;
  • /server目錄:類庫只能被Tomcat可見;由 Catalina ClassLoader類載入器載入目錄下的類庫;
  • /shared目錄:類庫對所有Web應用程式可見,但對Tomcat不可見;由 Shared ClassLoader類載入器載入目錄下的類庫;
  • /WebApp/WEB-INF目錄:僅僅對當前web應用程式可見。由 WebApp ClassLoader類載入器載入目錄下的類庫;
  • 每一個JSP檔案對應一個JSP類載入器。

參考:《深入理解Java虛擬機器》
MyBlog:https://nomico271.github.io/2017/07/07/JVM%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/
2017/07/05 In NJ

相關文章