Java類加載機制淺析

翻譯自jvm-works-jvm-architecture,並參考JVM 的類初始化機制java

JVM(Java Virtual Machine)做爲一個運行時引擎去運行Java應用。JVM 是實際調用main方法的對象。JVM是JRE(Java Runtime Enviroment)的一部分。程序員

Java應用被稱爲WORA(Write Once Run Anywhere).這意味着程序員能夠在一個系統上編寫Java程序而且預期能夠不須要任何修改就運行在Java可以運行的系統中。這之因此可行就是由於JVM。bootstrap

類加載子系統

它主要有如下幾個步驟:segmentfault

  • 加載
  • 連接
  • 初始化

加載

類加載器(class loader)讀取.class文件生成相應的二進制數據,並存儲在_方法區_中。對於每個.class文件,JVM存儲了一下幾個信息在方法區:數據結構

  • 加載的類及其父類的全限定名稱(Fully qualified name)。(全限定名稱:包含這個類所來自的包名,能夠用類的getName()方法得到)
  • .class文件是否與類/接口/枚舉相關
  • 修飾符,變量和方法信息等等。

在加載.class文件後,JVM在_堆內存_中建立了一個類型爲Class的一個對象去表示這個文件。請注意,這個對象的類型是`java.lang`包中預約義的Class類型。這個Class對象能夠被用於獲取類級別的信息,如類名,父類名,方法和變量信息等等。咱們可使用Object類的_getClass()_ 去獲取對象的引用。dom

總結爲:jvm

  • 根據類全名=》生成二進制字節碼
  • 將字節碼解析成方法區對應的數據結構儲存在方法區
  • 堆內存中生成Class類的實例
// A Java program to demonstrate working of a Class type 
// object created by JVM to represent .class file in 
// memory. 
import java.lang.reflect.Field; 
import java.lang.reflect.Method; 

// Java code to demonstrate use of Class object 
// created by JVM 
public class Test { 
	public static void main(String[] args) { 
		Student s1 = new Student(); 

		// Getting hold of Class object created 
		// by JVM. 
		Class c1 = s1.getClass(); 

		// Printing type of object using c1. 
		System.out.println(c1.getName()); 

		// getting all methods in an array 
		Method m[] = c1.getDeclaredMethods(); 
		for (Method method : m) 
			System.out.println(method.getName()); 

		// getting all fields in an array 
		Field f[] = c1.getDeclaredFields(); 
		for (Field field : f) 
			System.out.println(field.getName()); 
	} 
} 

// A sample class whose information is fetched above using 
// its Class object. 
class Student { 
	private String name; 
	private int roll_No; 

	public String getName() { return name; } 
	public void setName(String name) { this.name = name; } 
	public int getRoll_no() { return roll_No; } 
	public void setRoll_no(int roll_no) { 
		this.roll_No = roll_no; 
	} 
} 

複製代碼

輸出:函數

Student
getName
setName
getRoll_no
setRoll_no
name
roll_No
複製代碼

注意:每個加載的`.class`文件都只有一個Class對象被建立(即單例)性能

一般來講,有三種類加載器(Class loader):fetch

  • **Bootstrap class loader:**每個JVM的實現必須有一個bootstrap class loader, 它可以去加載可信的類。它加載存在於JAVA_HOME/jre/lib目錄下的java核心API類。這也是bootstrap的路徑。它是由原生語言(native languages)如C/C++實現的。
  • **擴展類加載器(EXtension class loader):**它是bootstrap class loader的子類。它加載存在於額外目錄(JAVA_HOME/jre/lib/ext)或者其餘由java.ext.dirs所指定的系統屬性。它是基於java由 sun.misc.Launcher$ExtClassLoader 類實現的。
  • **系統/應用類加載器(System/Application class loader):**它是額外類加載器的子類。它負責從應用的類路徑下加載類。它在內部使用映射到java.class.path的環境變量。它也是基於java由 sun.misc.Launcher$ExtClassLoader 類實現的。

JVM 中除了最頂層的Boostrap ClassLoader是用 C/C++ 實現外,其他類加載器均由 Java 實現,咱們能夠用getClassLoader方法來獲取當前類的類加載器:

// Java code to demonstrate Class Loader subsystem 
public class Test { 
	public static void main(String[] args) { 
		// String class is loaded by bootstrap loader, and 
		// bootstrap loader is not Java object, hence null 
		System.out.println(String.class.getClassLoader()); 

		// Test class is loaded by Application loader 
		System.out.println(Test.class.getClassLoader()); 
	} 
}	 

複製代碼

Output:

null
sun.misc.Launcher$AppClassLoader@73d16e93
複製代碼
java -verbose:class Test
[Opened C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.io.Serializable from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Comparable from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.CharSequence from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.String from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.reflect.Type from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
...
[Loaded java.lang.Void from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
null
[Loaded Test from file:/D:/chenyue/Learn/jvm/target/classes/]
sun.misc.Launcher$AppClassLoader@73d16e93
[Loaded java.lang.Shutdown from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jre1.8.0_231\lib\rt.jar]


複製代碼

注意:JVM遵循委託層級原則去加載類。系統類加載器將加載請求委託給擴展類加載器,擴展類加載器將請求委託給引導類加載器(boot-strap class loader)。若是這個類被髮如今引導路徑中,類將會被加載,除非請求被再次轉發給擴展類加載器,而後再轉發到系統加載器上。最後若是系統加載器加載類失敗,那咱們將會獲得一個運行時異常 java.lang.ClassNotFoundException.

連接

執行驗證,準備和(可選)解決方案。

  • 驗證(Verification):它保證`.class`文件的正確性即檢查文件是否被有效的編譯器正確格式化和生成。若是驗證失敗,咱們會獲得一個運行時異常_java.lang.VerigyError。_主要包含但不限於:

    • bytecode 的完整性(integrity)
    • 檢查final類沒有被繼承,final方法沒有被覆蓋
    • 確保沒有不兼容的方法簽名
  • 準備(Preparation):JVM爲類變量分配內存並初始化內存賦予默認值。

    在這個階段,JVM 也可能會爲有助於提升程序性能的數據結構分配內存,常見的一個稱爲method table的數據結構,它包含了指向全部類方法(也包括也從父類繼承的方法)的指針,這樣再調用父類方法時就不用再去搜索了。

  • 解決(Resolution): 確認類、接口、屬性和方法在類run-time constant pool的位置,用以將_符號引用(Symbolic References)_變爲直接引用。這經過搜索方法區來定位引用的實體來實現。 *

初始化

在這個階段,沒有的靜態變量和靜態代碼塊(若是存在)都被賦予定義在代碼中的數值。初始化的執行順序在一個類中是自頂向下的,在類的層次關係中是從父類到子類。

**第一次 主動調用**某類的最後一步是Initialization,這個過程會去按照代碼書寫順序進行初始化,這個階段會去真正執行代碼,注意包括:代碼塊(static與非static)、構造函數、變量顯式賦值。若是一個類有父類,會先去執行父類的initialization階段,而後在執行本身的。

上面這段話有兩個關鍵詞:第一次主動調用第一次是說只在第一次時纔會有初始化過程,之後就不須要了,能夠理解爲每一個類有且僅有一次初始化的機會。那麼什麼是主動調用呢?
JVM 規定了如下六種狀況爲主動調用,其他的皆爲被動調用

  1. 一個類的實例被建立(new操做、反射、cloning,反序列化)
  2. 調用類的static方法
  3. 使用或對類/接口的static屬性進行賦值時(這不包括final的與在編譯期肯定的常量表達式)
  4. 當調用 API 中的某些反射方法時
  5. 子類被初始化
  6. 被設定爲 JVM 啓動時的啓動類(具備main方法的類)

本文後面會給出一個示例用於說明主動調用被動調用區別。

在這個階段,執行代碼的順序遵循如下兩個原則:

  1. 有static先初始化static,而後是非static的
  2. 顯式初始化,構造塊初始化,最後調用構造函數進行初始化

示例

屬性在不一樣時期的賦值

class Singleton {

    private static Singleton mInstance = new Singleton();// 位置1
    public static int counter1;
    public static int counter2 = 0;

// private static Singleton mInstance = new Singleton();// 位置2

    private Singleton() {
        counter1++;
        counter2++;
    }

    public static Singleton getInstantce() {
        return mInstance;
    }
}

public class InitDemo {

    public static void main(String[] args) {

        Singleton singleton = Singleton.getInstantce();
        System.out.println("counter1: " + singleton.counter1);
        System.out.println("counter2: " + singleton.counter2);
    }
}
複製代碼

mInstance在位置1時,打印出

counter1: 1
counter2: 0
複製代碼

mInstance在位置2時,打印出

counter1: 1
counter2: 1
複製代碼

Singleton中的三個屬性在Preparation階段會根據類型賦予默認值,在Initialization階段會根據顯示賦值的表達式再次進行賦值(按順序自上而下執行)。根據這兩點,就不難理解上面的結果了。

主動調用 vs. 被動調用

class NewParent {

    static int hoursOfSleep = (int) (Math.random() * 3.0);

    static {
        System.out.println("NewParent was initialized.");
    }
}

class NewbornBaby extends NewParent {

    static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);

    static {
        System.out.println("NewbornBaby was initialized.");
    }
}

public class ActiveUsageDemo {

    // Invoking main() is an active use of ActiveUsageDemo
    public static void main(String[] args) {

        // Using hoursOfSleep is an active use of NewParent,
        // but a passive use of NewbornBaby
        System.out.println(NewbornBaby.hoursOfSleep);
    }

    static {
        System.out.println("ActiveUsageDemo was initialized.");
    }
}
複製代碼

上面的程序最終輸出:

ActiveUsageDemo was initialized.
NewParent was initialized.
1
複製代碼

之因此沒有輸出NewbornBaby was initialized.是由於沒有主動去調用NewbornBaby,若是把打印的內容改成NewbornBaby.hoursOfCrying 那麼這時就是主動調用NewbornBaby了,相應的語句也會打印出來。

首次主動調用纔會初始化

public class Alibaba {

    public static int k = 0;
    public static Alibaba t1 = new Alibaba("t1");
    public static Alibaba t2 = new Alibaba("t2");
    public static int i = print("i");
    public static int n = 99;
    private int a = 0;
    public int j = print("j");
    {
        print("構造塊");
    }
    static {
        print("靜態塊");
    }

    public Alibaba(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++i;
        ++n;
    }

    public static int print(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++n;
        return ++i;
    }

    public static void main(String args[]) {
        Alibaba t = new Alibaba("init");
    }
}
複製代碼

上面這個例子是阿里巴巴在14年的校招附加題,我當時看到這個題,就以爲與阿里無緣了。囧

1:j   i=0    n=0
2:構造塊   i=1    n=1
3:t1   i=2    n=2
4:j   i=3    n=3
5:構造塊   i=4    n=4
6:t2   i=5    n=5
7:i   i=6    n=6
8:靜態塊   i=7    n=99
9:j   i=8    n=100
10:構造塊   i=9    n=101
11:init   i=10    n=102
複製代碼

上面是程序的輸出結果,下面我來一行行分析之。

  1. 因爲Alibaba是 JVM 的啓動類,屬於主動調用,因此會依此進行 loading、linking、initialization 三個過程。

  2. 通過 loading與 linking 階段後,全部的屬性都有了默認值,而後進入最後的 initialization 階段。

  3. 在 initialization 階段,先對 static 屬性賦值,而後在非 static 的。k 第一個顯式賦值爲 0 。

  4. 接下來是t1屬性,因爲這時Alibaba這個類已經處於 initialization 階段,static 變量無需再次初始化了,因此忽略 static 屬性的賦值,只對非 static 的屬性進行賦值,全部有了開始的:

    1:j   i=0    n=0
        2:構造塊   i=1    n=1
        3:t1   i=2    n=2
    複製代碼
  5. 接着對t2進行賦值,過程與t1相同

    4:j   i=3    n=3
        5:構造塊   i=4    n=4
        6:t2   i=5    n=5
    複製代碼
  6. 以後到了 static 的 in

    7:i   i=6    n=6
    複製代碼
  7. 到如今爲止,全部的static的成員變量已經賦值完成,接下來就到了 static 代碼塊

    8:靜態塊   i=7    n=99
    複製代碼
  8. 至此,全部的 static 部分賦值完畢,接下來是非 static 的 j

    9:j   i=8    n=100
    複製代碼
  9. 全部屬性都賦值完畢,最後是構造塊與構造函數

    10:構造塊   i=9    n=101
        11:init   i=10    n=102
    複製代碼

通過上面這9步,Alibaba這個類的初始化過程就算完成了。這裏面比較容易出錯的是第3步,認爲會再次初始化 static 變量或代碼塊。而其實是不必,不然會出現屢次初始化的狀況。

但願你們能多思考思考這個例子的結果,加深這三個過程的理解。

小結

a. 加載類

  1. 爲父類靜態屬性分配內存並賦值 / 執行父類靜態代碼段 (按代碼順序)
  2. 爲子類靜態屬性分配內存並賦值 / 執行子類靜態代碼段 (按代碼順序)

b. 建立對象

  1. 爲父類實例屬性分配內存並賦值 / 執行父類非靜態代碼段 (按代碼順序)
  2. 執行父類構造器
  3. 爲子類實例屬性分配內存並賦值 / 執行子類非靜態代碼段 (按代碼順序)
  4. 執行子類構造器

參考

相關文章
相關標籤/搜索