類加載過程示例


本篇的目的,是通過具體代碼解釋jvm類加載的詳細過程。再結合之前的理論說明,完全可以應對所有類加載的相關問題

類加載的整體說明,請參看 《JVM類加載總結》

1、準備試驗用代碼

父類:

public class ParentClass {

    public static final String PARENT_FINAL_P = "父類常量";
    public static String parentP = "父類成員變量";

    static{
        System.out.println("父類靜態代碼塊");
    }

    {
        System.out.println("父類動態代碼塊");
    }

    public ParentClass() {
        System.out.println("父類構造方法");
    }

    public static void parentStaticMethod(){
        System.out.println("父類靜態方法");
    }

    private String propertityA;

    public String getPropertityA() {
        return propertityA;
    }

    public void setPropertityA(String propertityA) {
        this.propertityA = propertityA;
    }
}

子類:

public class ChildClass extends ParentClass{

    public static final String CHILD_FINAL_P = "子類常量";
    public static String childP = "子類成員變量";

    static{
        System.out.println("子類靜態代碼塊");
    }

    {
        System.out.println("子類動態代碼塊");
    }

    static{
        System.out.println("子類靜態代碼塊2");
    }

    public ChildClass() {
        System.out.println("子類構造方法");
    }

    public static void childStaticMethod(){
        System.out.println("子類靜態方法");
    }

    private String propertityB;

    public String getPropertityB() {
        return propertityB;
    }

    public void setPropertityB(String propertityB) {
        this.propertityB = propertityB;
    }

}

測試類:
(測試用的main()方法不能寫在父類或者子類裏面。原因:jvm會強制加載main()方法所在的類,這樣會影響測試效果)

public class TestClass {

    static {
        System.out.println("測試類靜態代碼塊");
    }

    {
        System.out.println("測試類動態代碼塊");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        這裏的內容下面分情況來寫
    }

}

2、引用常量不會引發類加載

    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(ChildClass.PARENT_FINAL_P);
    }
    
測試類靜態代碼塊
父類常量    

可以看到,引用的父類的常量(final),也不會觸發子類的任何加載行爲(當然父類也沒有被加載)

3、引用父類的成員變量或是靜態方法會執行初始化

    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(ChildClass.parentP);
    }
    
測試類靜態代碼塊
父類靜態代碼塊
父類成員變量    
    

子類引用父類的成員變量時,觸發了父類的初始化(“父類靜態代碼塊”被執行),但是子類本身沒有被初始化

    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(ChildClass.childP);
    }
    
測試類靜態代碼塊
父類靜態代碼塊
子類靜態代碼塊
子類靜態代碼塊2
子類成員變量    

子類引用自身成員變量時,根據“虛擬機會保證首先執行父類的()方法”的理論,父類先被初始化(“父類靜態代碼塊”),然後再初始化子類(“子類靜態代碼塊1、2”)

另外,關於靜態變量、靜態代碼塊初始化的執行順序,代碼中先定義的先執行(變量、動態代碼塊同理)

4、使用new關鍵字實例化對象,會初始化類,並觸發對象的一系列初始化動作

    public static void main(String[] args) throws ClassNotFoundException {
        ChildClass child = new ChildClass();
    }
    
    
測試類靜態代碼塊
父類靜態代碼塊
子類靜態代碼塊
子類靜態代碼塊2
父類動態代碼塊
父類構造方法
子類動態代碼塊
子類構造方法    

可以看到,與第3節相比,不但執行了類加載(clinit()方法調用靜態代碼),而且先創建了父類對象(父類動態代碼塊、父類構造方法),然後再創建的子類對象(子類動態代碼塊、子類構造方法 )

所以在一些文章中,給出了這樣的對象初始化順序:

靜態變量/靜態代碼塊(先父類後子類) > main方法 > 變量/動態代碼塊(先父類後子類) > 構造器(先父類後子類)

其實只要弄明白類的加載原理(包含一點最基礎的java內存模型知識),這種順序完全不用死記硬背

5、ClassLoader.loadClass

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        classLoader.loadClass("com.demo.ChildClass");
    }
    
測試類靜態代碼塊    

loadClass什麼也沒有做……

因爲這個public的loadClass,調用的是ClassLoader的

protected Class<?> loadClass(String name, boolean resolve)

方法,並且resolve固定傳false,意思是隻進行加載和鏈接,不執行初始化過程。

6、Class.forName

    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.demo.ChildClass");
    }
    
測試類靜態代碼塊
父類靜態代碼塊
子類靜態代碼塊
子類靜態代碼塊2    
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class.forName("com.demo.ChildClass", false, classLoader);
    }
測試類靜態代碼塊    

Class.forName的情況與ClassLoader.loadClass不同,有兩個重載方法。

只指定一個參數時,調用的是一個native方法

forName0(className, true, ClassLoader.getClassLoader(caller), caller);

其中第二個參數true,指定了要對類進行初始化;

而使用重載的三個參數的方法時,其方法定義是這樣的

public static Class<?> forName(String name, boolean initialize,ClassLoader loader)

第二個參數指定是否初始化,然後調用了同樣的native方法,將boolean變量傳遞了進去,傳false的話就不執行初始化過程了

7、類的加載過程只執行一次

    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(ChildClass.CHILD_FINAL_P);
        System.out.println(">>><<<");
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        classLoader.loadClass("com.demo.ChildClass");
        System.out.println(">>><<<");
        Class.forName("com.demo.ChildClass");
        System.out.println(">>><<<");
        ChildClass child = new ChildClass();
    }
    
測試類靜態代碼塊
子類常量
>>><<<
>>><<<
父類靜態代碼塊
子類靜態代碼塊
子類靜態代碼塊2
>>><<<
父類動態代碼塊
父類構造方法
子類動態代碼塊
子類構造方法 

最開始只引用了一下子類成員常量,因爲常量在方法區的常量池裏面,沒有觸發類的任何動作;

第二步 classLoader.loadClass 只做了類的加載和鏈接,不進行初始化,並沒有任何輸出;

第三步 Class.forName ,前面已經完成了加載和鏈接,所以跳過(輸出中無法清晰顯示出這個結論),只做了類的初始化;

第四步進行對象的創建和初始化,由於類的加載全過程已經完成,所以new關鍵字並沒有觸發各種靜態資源的初始化(因爲已經完事了)

簡單總結一下

  • 編譯時把常量都放進常量池,引用時不會涉及到類

  • 類加載時,除了非靜態成員變量不會被加載,其它的都會被加載(包括非靜態方法——這一結論無法用示例來說明),加載完畢初始化

  • 創建實例時,先執行動態方法塊,再執行構造方法

  • 先父類後子類

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