天天看點

Java 類加載機制詳解

本文主要講述 Java 類加載過程的七個階段:加載、驗證、準備、解析、初始化、使用、解除安裝。同時也介紹了類加載器,包括雙親委托機制,自定義類加載器,最後通過例子訓練和總結了類在加載過程中類的初始化順序。

引子

Java 虛拟機的啟動是通過引導類加載器 ( Bootstrap Class Loader ) 建立一個初始類  (Initial Class ) 來完成,這個類是由虛拟機的具體實作指定。緊接着,Java虛拟機連結這個初始類,初始化并調用它的 public void main(String[])方法。之後的整個執行過程都是由對此方法的調用開始。執行 main 方法中的 Java 虛拟機指令可能會導緻 Java 虛拟機連結另外的一些類或接口,也可能會調用另外的方法。

可能在某種 Java 虛拟機的實作上,初始類會作為指令行參數被提供給虛拟機。當然,虛拟機實作也可以利用一個初始類讓類加載器依次加載整個應用。初始類當然也可以選擇組合上述的方式來工作。

                                      —— 以上内容摘自《Java 虛拟機規範》(Java SE 7 版)

在講類的加載機制前,先來看一道題目:

public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println("爸爸的歲數:" + Son.factor);  //入口1
        // new Son();  //入口 2
    }
}

class Grandpa {
    static {
        System.out.println("爺爺在靜态代碼塊");
    }
    public Grandpa() {
        System.out.println("我是爺爺~");
    }
}    

class Father extends Grandpa {
    static {
        System.out.println("爸爸在靜态代碼塊");
    }
    public static int factor = 25;

    public Father() {
        System.out.println("我是爸爸~");
    }
}

class Son extends Father {
    static {
        System.out.println("兒子在靜态代碼塊");
    }

    public Son() {
        System.out.println("我是兒子~");
    }
}      

上面的代碼中分了入口1和入口2, 兩者不同時存在,入口不一樣,最後輸出的結果也是不一樣的。小夥伴可以思考下這兩個入口對于類的初始化有啥不一樣。下面是具體結果:

入口1 的結果:

爺爺在靜态代碼塊
爸爸在靜态代碼塊
爸爸的歲數:25      

入口2 的結果

爺爺在靜态代碼塊
爸爸在靜态代碼塊
兒子在靜态代碼塊
我是爺爺~
我是爸爸~
我是兒子~      

如果以前沒有遇到這種問題,現在要你解答肯定是很難的。該題考察的就是你對 Java 類加載機制的了解。如果你對 Java 加載機制不了解,那麼你是無法解答這道題目的。

對比上面兩個結果,可以發現,入口1 都是靜态代碼的初始化,入口2 既涉及到靜态代碼的初始化,也涉及到類的初始化。到此大家肯定就知道對于靜态代碼和非靜态代碼的初始化邏輯是有差別的。

這篇文章,将對 Java 類加載機制的進行講解,讓你以後遇到類似問題不在犯難。

類的加載過程

當 Java 虛拟機将 Java 源碼編譯為位元組碼之後,虛拟機便可以将位元組碼讀取進記憶體,進而進行解析、運作等整個過程,這個過程我們叫:Java 虛拟機的類加載機制。JVM 虛拟機執行 class 位元組碼的過程可以分為七個階段:加載、驗證、準備、解析、初始化、使用、解除安裝。其中加載、檢驗、準備、初始化和解除安裝這個五個階段的順序是固定的,而解析則未必。為了支援動态綁定,解析這個過程可以發生在初始化階段之後。

1. 加載

什麼情況下需要開始類加載的第一個階段:加載。 JAVA虛拟機規範并沒有進行強制限制,交給虛拟機的具體實作自由把握。

加載階段是“類加載”過程中的一個階段,這個階段通常也被稱作“裝載”,在加載階段,虛拟機主要完成以下3件事情:

  1. 通過 "類全名" 來擷取定義此類的二進制位元組流
  2. 将位元組流所代表的靜态存儲結構轉換為方法區的運作時資料結構
  3. 在 java 堆中生成一個代表這個類的 java.lang.Class 對象,作為方法區這些資料的通路入口(是以我們能夠通過低調用類.getClass() )

注意這裡位元組流不一定非得要從一個 Class 檔案擷取,這裡既可以從 ZIP 包中讀取(比如從 jar 包和 war 包中讀取),也可以在運作時計算生成(動态代理),也可以由其它檔案生成(比如将 JSP 檔案轉換成對應的 Class 類)。加載的資訊存儲在 JVM 的方法區。

對于數組類來說,它并沒有對應的位元組流,而是由 Java 虛拟機直接生成的。對于其它的類來說,Java 虛拟機則需要借助類加載器來完成查找位元組流的過程。

如果上面那麼多記不住: 請一定記住這句: 加載階段也就是查找擷取類的二進制資料(磁盤或者網絡)動作,将類的資料(Class 的資訊:類的定義或者結構)放入方法區 (記憶體)。

一圖說明:

Java 類加載機制詳解

2. 驗證

驗證的主要作用就是確定被加載的類的正确性。也是連接配接階段的第一步。說白了也就是我們加載好的 .class 檔案不能對我們的虛拟機有危害,是以先檢測驗證一下。他主要是完成四個階段的驗證:

  1. 檔案格式的驗證:驗證 .class 檔案位元組流是否符合 class 檔案的格式的規範,并且能夠被目前版本的虛拟機處理。這裡面主要對魔數、主版本号、常量池等等的校驗(魔數、主版本号都是 .class 檔案裡面包含的資料資訊、在這裡可以不用了解)。
  2. 中繼資料驗證:主要是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合 java 語言規範的要求,比如說驗證這個類是不是有父類,類中的字段方法是不是和父類沖突等等。
  3. 位元組碼驗證:這是整個驗證過程最複雜的階段,主要是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。在中繼資料驗證階段對資料類型做出驗證後,這個階段主要對類的方法做出分析,保證類的方法在運作時不會做出威海虛拟機安全的事。
  4. 符号引用驗證:它是驗證的最後一個階段,發生在虛拟機将符号引用轉化為直接引用的時候。主要是對類自身以外的資訊進行校驗。目的是確定解析動作能夠完成。

對整個類加載機制而言,驗證階段是一個很重要但是非必需的階段,如果我們的代碼能夠確定沒有問題,那麼我們就沒有必要去驗證,畢竟驗證需要花費一定的的時間。當然我們可以使用 -Xverfity:none 來關閉大部分的驗證。

3. 準備(重點)

當完成位元組碼檔案的校驗之後,JVM 便會開始為類變量配置設定記憶體并初始化。這裡需要注意兩個關鍵點,即記憶體配置設定的對象以及初始化的類型。

  • 記憶體配置設定的對象。Java 中的變量有「類變量」和「類成員變量」兩種類型,「類變量」指的是被 static 修飾的變量,而其他所有類型的變量都屬于「類成員變量」。在準備階段,JVM 隻會為「類變量」配置設定記憶體,而不會為「類成員變量」配置設定記憶體。「類成員變量」的記憶體配置設定需要等到初始化階段才開始。

例如下面的代碼在準備階段,隻會為 factor 屬性配置設定記憶體,而不會為 website 屬性配置設定記憶體。

public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";      
  • 初始化的類型。在準備階段,JVM 會為類變量配置設定記憶體,并為其初始化。但是這裡的初始化指的是為變量賦予 Java 語言中該資料類型的零值,而不是使用者代碼裡初始化的值。

例如下面的代碼在準備階段之後,sector 的值将是 0,而不是 3。

public static int sector = 3;      

但如果一個變量是常量(被 static final 修飾)的話,那麼在準備階段,屬性便會被賦予使用者希望的值。例如下面的代碼在準備階段之後,number 的值将是 3,而不是 0。

public static final int number = 3;      

之是以 static final 會直接被複制,而 static 變量會被賦予零值。其實我們稍微思考一下就能想明白了。

兩個語句的差別是一個有 final 關鍵字修飾,另外一個沒有。而 final 關鍵字在 Java 中代表不可改變的意思,意思就是說 number 的值一旦指派就不會在改變了。既然一旦指派就不會再改變,那麼就必須一開始就給其賦予使用者想要的值,是以被 final 修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final 修飾的類變量,其可能在初始化階段或者運作階段發生變化,是以就沒有必要在準備階段對它賦予使用者想要的值。 

4. 解析

解析階段是虛拟機常量池内的符号引用替換為直接引用的過程。

符号引用:符号引用是一組符号來描述所引用的目标對象,符号可以是任何形式的字面量,隻要使用時能無歧義地定位到目标即可。符号引用與虛拟機實作的記憶體布局無關,引用的目标對象并不一定已經加載到記憶體中。Java 虛拟機明确在 Class 檔案格式中定義的符号引用的字面量形式。

直接引用:直接引用可以是直接指向目标對象的指針、相對偏移量或是一個能間接定位到目标的句柄。直接引用是與虛拟機記憶體布局實作相關的,同一個符号引用在不同虛拟機執行個體上翻譯出來的直接引用一般不會相同,如果有了直接引用,那引用的目标必定已經在記憶體中存在。

在解析的階段,解析動作主要針對7類符号引用進行,它們的名稱以及對于常量池中的常量類型和解析報錯資訊如下:

| 解析動作 | 符号引用 | 解析可能的報錯 | | ---------- | ------------------------------- | -----------------------------------------------------------

| | 類或接口 | CONSTANTClassInfo | java.land.IllegalAccessError

| | 字段 | CONSTANTFieldrefInfo | java.land.IllegalAccessError 或 java.land.NoSuchFieldError

| | 類方法 | CONSTANTMethodefInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError

| | 接口方法 | CONSTANTInterfaceMethoderInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError

| | 方法類型 | CONSTANTMethodTypeInfo |

| | 方法句柄 | CONSTANTMethodhandlerInfo |

| | 調用限定符 | CONSTANTInvokeDynamicInfo | 

解析的整個階段在虛拟機中還是比較複雜的,遠比上面介紹的複雜的多,但是很多特别細節的東西我們可以暫時先忽略,先有個大概的認識和了解之後有時間在慢慢深入了。

5. 初始化(重點)

類初始階段是類加載過程的最後一步,在上面提到的類加載過程中,除了加載階段使用者應用程式可以通過自定義類加載器參與之外,其餘的動作全部由虛拟機主導和控制。初始化階段,是真正開始執行類中定義的 Java 程式代碼(或者說是位元組碼)。

在準備階段,變量已經指派過一次系統要求的初始值(零值),而在初始化階段,則根據程式員通過程式制定的主觀計劃去初始化類變量和其他資源。(或者從另一個角度表達:初始化階段是執行類構造器 

<clinit>() 

方法的過程。)

在這個階段,JVM 會根據語句執行順序對類對象進行初始化,一般來說當 JVM 遇到下面 5 種情況的時候會觸發初始化:

  1. 遇到 new、getstatic、putstatic、invokestatic 這四條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的 Java 代碼場景是:使用new 關鍵字執行個體化對象的時候、讀取或設定一個類的靜态字段(被 final 修飾、已在編譯器把結果放入常量池的靜态字段除外)的時候,以及調用一個類的靜态方法的時候。
  2. 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛拟機啟動時,使用者需要指定一個要執行的主類(包含 main() 方法的那個類),虛拟機會先初始化這個主類。
  5. 當使用 JDK1.7 動态語言支援時,如果一個 java.lang.invoke.MethodHandle 執行個體最後的解析結果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。

看到上面幾個條件你可能會暈了,但是不要緊,不需要背,知道一下就好,後面用到的時候回到找一下就可以了。 

注意這裡的初始化,并不是說創造的類的執行個體,而是執行了類構造器,簡單來說就是隻對靜态變量,靜态代碼塊進行初始化。對于構造函數隻有在建立執行個體的時候才會執行。

6. 使用

當 JVM 完成初始化階段之後,JVM 便開始從入口方法開始執行使用者的程式代碼。這個階段也隻是了解一下就可以。

7. 解除安裝

當使用者程式代碼執行完畢後,JVM 便開始銷毀建立的 Class 對象,最後負責運作的 JVM 也退出記憶體。這個階段也隻是了解一下就可以。

8. 引子題目解答

還記得前面的題目嘛,下面開始分析:

入口1 

也許會有人問為什麼沒有輸出「兒子在靜态代碼塊」這個字元串?

這是因為對于靜态字段,隻有直接定義這個字段的類才會被初始化(執行靜态代碼塊)。是以通過其子類來引用父類中定義的靜态字段,隻會觸發父類的初始化而不會觸發子類的初始化。

對面上面的這個例子,我們可以從入口開始分析一路分析下去:

  • 首先程式到 main 方法這裡,使用标準化輸出 Son 類中的 factor 類成員變量,但是 Son 類中并沒有定義這個類成員變量。于是往父類去找,我們在 Father 類中找到了對應的類成員變量,于是觸發了 Father 的初始化。
  • 但根據我們上面說到的初始化的 5 種情況中的第 3 種(當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化)。我們需要先初始化 Father 類的父類,也就是先初始化 Grandpa 類再初始化 Father 類。于是我們先初始化 Grandpa 類輸出:「爺爺在靜态代碼塊」,再初始化 Father 類輸出:「爸爸在靜态代碼塊」。
  • 最後,所有父類都初始化完成之後,Son 類才能調用父類的靜态變量,進而輸出:「爸爸的歲數:25」。

入口2 

這裡采用 new 進行初始化,是以先進行父類得初始化。先是執行靜态變量初始化。子類建立對象的同時會先創造父類的對象,是以必須先調用父類的構造方法。

變動

這裡我做了一些改變:

public class ClassLoaderTest {
    public static void main(String[] args) {
        // System.out.println("爸爸的歲數:" + Son.factor);  //入口1
         new Son(3);  //入口 2
    }

}

class Grandpa {
    int s = 3;
  
    public Grandpa(int s) {
        System.out.println("我是爺爺~" );
    }
    static {
        System.out.println("爺爺在靜态代碼塊");
    }
}    

class Father extends Grandpa {
    static {
        System.out.println("爸爸在靜态代碼塊");
    }
    public static int factor = 25;

    public Father(int s) {
        //super(s);
        System.out.println("我是爸爸~");
    }
}

class Son extends Father {
    static {
        System.out.println("兒子在靜态代碼塊");
    }

    public Son(int s ) {
        super(s);
        System.out.println("我是兒子~");
    }
}      

這裡的變動是,父類子類都隻有一個有參構造函數,在初始化子類得時候,不顯示的調用父類的構造函數,運作結果如下:

爺爺在靜态代碼塊 Exception in thread "main" 
爸爸在靜态代碼塊
兒子在靜态代碼塊
Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
    Implicit super constructor Grandpa() is undefined. Must explicitly invoke another constructor

    at Father.<init>(ClassLoaderTest.java:27)
    at Son.<init>(ClassLoaderTest.java:39)
    at ClassLoaderTest.main(ClassLoaderTest.java:5)      

簡單來說,如果子類構造函數不顯示調用父類的構造函數,這時候在初始化子類得時候,就會去父類尋找無參構造函數,如果父類隻定義了有參構造函數,沒有無參構造函數,就會報錯。是以一般來說最好是顯示調用,又或者多定義幾種不同的構造函數,友善在不同場景下調用。

類加載器

把類加載階段的 "通過一個類的全限定名來擷取描述此類的二進制位元組流" 這個動作交給虛拟機之外的類加載器來完成。這樣的好處在于,我們可以自行實作類加載器來加載其他格式的類,隻要是二進制位元組流就行,這就大大增強了加載器靈活性。

系統自帶的類加載器分為三種:

  1. 啟動類加載器。其它的類加載器都是 java.lang.ClassLoader 的子類,啟動類加載器是由 C++ 實作的,沒有對應的 Java 對象,是以在 Java 中隻能用 null 代替。啟動類加載器加載最為基礎,最為重要的類,如 JRE 的 lib 目錄下 jar 包中的類;擴充類加載器的父類是啟動類加載器,它負責加載相對次要,但又通用的類,如 JRE 的 lib/ext 目錄下jar包中的類
  2. 擴充類加載器。Java核心類庫提供,負責加載java的擴充庫(加載 JAVA_HOME/jre/ext/*.jar 中的類),開發者可以直接使用擴充類加載器。
  3. 應用程式類加載器。Java核心類庫提供。應用類加載器的父類加載器則是擴充類加載器,它負責加載應用程式路徑下的類。開發者可以直接使用這個類加載器,若應用程式中沒有定義過自己的類加載器,java 應用的類都是由它來完成加載的。

具體關系如下:

雙親委派機制工作過程:

如果一個類加載器收到了類加載器的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父加載器去完成。每個層次的類加載器都是如此,是以所有的加載請求最終都會傳送到 Bootstrap 類加載器(啟動類加載器)中,隻有父類加載回報自己無法加載這個請求(它的搜尋範圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

雙親委派模型的優點:java類随着它的加載器一起具備了一種帶有優先級的層次關系.

例如類 java.lang.Object 它存放在 rt.jart 之中,無論哪一個類加載器都要加載這個類.最終都是雙親委派模型最頂端的 Bootstrap 類加載器去加載.是以Object類在程式的各種類加載器環境中都是同一個類.相反.如果沒有使用雙親委派模型.由各個類加載器自行去加載的話.如果使用者編寫了一個稱為 "java.lang.Object" 的類,并存放在程式的 ClassPath 中。那系統中将會出現多個不同的Object類,java類型體系中最基礎的行為也就無法保證,應用程式也将會一片混亂。

這裡也可以用代碼驗證下:

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@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
null      

跟前面的描述是一緻的。啟動類加載器是由 C++ 實作的,沒有對應的 Java 對象,是以在 Java 中隻能用 null 代替。

自定義類加載器

1、為什麼要自定義ClassLoader

因為系統的 ClassLoader 隻會加載指定目錄下的 class 檔案,如果你想加載自己的 class 檔案,那麼就可以自定義一個 ClassLoader.

而且我們可以根據自己的需求,對 class 檔案進行加密和解密。

2. 如何自定義ClassLoader

建立一個類繼承自 java.lang.ClassLoader 重寫它的 findClass 方法。将 class 位元組碼數組轉換為 Class 類的執行個體。調用 loadClass 方法加載即可

代碼實戰:

先是定義一個自定義類加載器

package com.hello.test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
    // 指定路徑
    private String path ;
    
    public MyClassLoader(String classPath){
        path=classPath;
    }
 
    /**
     * 重寫findClass方法
     * @param name 是我們這個類的全路徑
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class log = null;
        // 擷取該class檔案位元組碼數組
        byte[] classData = getData();
 
        if (classData != null) {
            // 将class的位元組碼數組轉換成Class類的執行個體
            log = defineClass(name, classData, 0, classData.length);
        }
        return log;
    }
 
    /**
     * 将class檔案轉化為位元組碼數組
     * @return
     */
    private byte[] getData() {
 
        File file = new File(path);
        if (file.exists()){
            FileInputStream in = null;
            ByteArrayOutputStream out = null;
            try {
                in = new FileInputStream(file);
                out = new ByteArrayOutputStream();
 
                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = in.read(buffer)) != -1) {
                    out.write(buffer, 0, size);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                } catch (IOException e) {
 
                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        }else{
            return null;
        }
    }
}      

可以再 getData 裡面做很多事情 ,比如加密解密之類的 都是可以的。

接着建立一個試驗 class :

package com.hello.test;

public class Log {
    public static void main(String[] args) {
        System.out.println("load Log class successfully from log " );
    }
}      

執行指令行 javac Log.java 生成我們的 Log.class 檔案:

Java 類加載機制詳解

最後就是進行加載:

package com.hello.test;

import java.lang.reflect.Method;

public class ClassLoaderTest {
    public static void main(String[] args) {
         // 這個類class的路徑,自己複制自己電腦的路徑
        String classPath = "/Users/yourname/Documents/workspace-sts-3.9.6.RELEASE/HelloWorld/src/Log.class";
 
        MyClassLoader myClassLoader = new MyClassLoader(classPath);
        // 類的全稱,對應包名
        String packageNamePath = "com.hello.test.Log";
        try {
        // 加載Log這個class檔案
            Class<?> Log = myClassLoader.loadClass(packageNamePath);
            System.out.println("類加載器是:" + Log.getClassLoader());
            // 利用反射擷取main方法
            Method method = Log.getDeclaredMethod("main", String[].class);
            Object object = Log.newInstance();
            String[] arg = {"ad"};
            method.invoke(object, (Object) arg);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}      

輸出結果如下:

Java 類加載機制詳解

可以看到是委托父類進行加載的。 到此,關于類加載器的内容就說完了。

鞏固練習

最後我們再來看一道更新過後的題目:

public class Book {
 
    static int amount1 = 112;

    static Book book = new Book();  // 入口1


    public static void main(String[] args) {
        staticFunction();
    }


    static {
        System.out.println("書的靜态代碼塊");
    }

    {
        System.out.println("書的普通代碼塊");
    }

    Book() {
        System.out.println("書的構造方法");
        System.out.println("price=" + price +", amount=" + amount + ", amount1=" + amount1);
    }

    public static void staticFunction() {
        System.out.println("書的靜态方法");
      
     System.out.println("amount=" + amount + ",amount1=" + amount1);      
}
    
    int price = 110;
    static int amount = 112;
    // static Book book = new Book(); // 入口2
}      

 入口1 的結果

書的普通代碼塊

書的構造方法

price=110, amount=0, amount1=112

書的靜态代碼塊

書的靜态方法

amount=112, amount1=112

price=110, amount=112, amount1=112

 入口1 分析

在上面兩個例子中,因為 main 方法所在類并沒有多餘的代碼,我們都直接忽略了 main 方法所在類的初始化。

但在這個例子中,main 方法所在類有許多代碼,我們就并不能直接忽略了。

  1. 當 JVM 在準備階段的時候,便會為類變量配置設定記憶體和進行初始化。此時,我們的 book 執行個體變量被初始化為 null,amount,amout1 變量被初始化為 0。
  2. 當進入初始化階段後,因為 Book 方法是程式的入口,根據我們上面說到的類初始化的五種情況的第四種(當虛拟機啟動時,使用者需要指定一個要執行的主類(包含 main() 方法的那個類),虛拟機會先初始化這個主類)。是以 JVM 會初始化 Book 類,即執行類構造器 。
  3. JVM 對 Book 類進行初始化首先是執行類構造器(按順序收集類中所有靜态代碼塊和類變量指派語句就組成了類構造器 ),
  4. 後執行對象的構造器(按順序收內建員變量指派和普通代碼塊,最後收集對象構造器,最終組成對象構造器 )。

對于入口1,執行類構造器發現 book 執行個體是靜态變量,于是就會執行普通代碼塊,再去執行 book 的構造函數。執行完後,重新回到執行類構造器的路上,對剩下的靜态變量進行初始化。

 入口2 分析

入口2 的變化就是将靜态執行個體初始化移到了最後。進而保證優先執行類構造器,再去進行對象初始化過程。

變例 

假如把入口1,2 都注釋掉,這回結果會怎麼樣:

書的靜态代碼塊
書的靜态方法
amount=112, amount1=112      

可以發現,最終隻有類構造器得到了執行。

方法論

從上面幾個例子可以看出,分析一個類的執行順序大概可以按照如下步驟:

  • 确定類變量的初始值。在類加載的準備階段,JVM 會為類變量初始化零值,這時候類變量會有一個初始的零值。如果是被 final 修飾的類變量,則直接會被初始成使用者想要的值。
  • 初始化入口方法。當進入類加載的初始化階段後,JVM 會尋找整個 main 方法入口,進而初始化 main 方法所在的整個類。當需要對一個類進行初始化時,會首先初始化類構造器(),之後初始化對象構造器()。
  • 初始化類構造器。JVM 會按順序收集類變量的指派語句、靜态代碼塊,最終組成類構造器由 JVM 執行。
  • 初始化對象構造器。JVM 會按照收內建員變量的指派語句、普通代碼塊,最後收集構造方法,将它們組成對象構造器,最終由 JVM 執行。

如果在初始化 main 方法所在類的時候遇到了其他類的初始化,那麼就先加載對應的類,加載完成之後傳回。如此反複循環,最終傳回 main 方法所在類。

參考文章

類加載機制-深入了解jvm

Java類加載機制,你了解了嗎?

JVM基礎系列第7講:JVM 類加載機制

Java記憶體管理-掌握虛拟機類加載機制(四)

樹林美麗、幽暗而深邃,但我有諾言尚待實作,還要奔行百裡方可沉睡。 -- 羅伯特·弗羅斯特