天天看點

JVM中類加載的過程

 前面看了類加載的時機,本文來記錄下類加載的過程,也就是加載的每個階段都做了哪些事情

類的生命周期

JVM中類加載的過程

加載

 "加載"是類加載過程中的一個階段,在這個階段虛拟機做了3件事

   通過一個類的全限定名擷取定義此類的二進制流

   通過這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構

   在記憶體中生成一個代表這個類的 java.lang.Class對象,作為方法區這個類的各種資料通路的入口。

JVM中類加載的過程

     注意,加載階段與連接配接階段的部分内容是交叉進行,加載階段尚未完成,連接配接階段可能已經開始了,但總體的順序還是先加載再連接配接。

驗證

 驗證階段是連接配接階段的第一步,這個階段的目的是確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求。不會危害虛拟機自身。驗證的内容包含如下4個階段

   檔案格式驗證

     驗證位元組流的格式是否符合class檔案的規範及是否能被目前虛拟機處理。

   a.是否已魔數0xCAFEBABE開頭

   b.主次版本号是否在目前虛拟機處理範圍之内

   c.常量池的常量中是否有不被支援的常量類型tag标志

   d.指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量

   e.CONSTANT_Utf8_Info型的常量是否有不符合UTF-8編碼的資料

   f.Class檔案中各個部分及檔案本身是否有被删除的或附加的其他資訊

   …

   中繼資料驗證

     語言分析,保證描述資訊符合java語言規範要做

   a.這個類是否有父類

   b.這個類的父類是否繼承了不允許繼承的類(final修飾)

   c.非抽象類,是否實作了父類及接口中的所有的抽象方法

   d.類中字段,方法是否和父類産生沖突

   位元組碼驗證

     本階段是最複雜的階段,通過資料流和控制流分析确定程式語義是否合法和符合邏輯。

   符号引用驗證

準備

 本階段也稱為零值階段,也就是将類中的類變量配置設定記憶體及賦初值,此處的初值是賦予對應類型的零值,如下

public static int value=123;      

那麼變量value的值在這個階段賦予的是0而不是123,這裡int為0,long為0L,boolean為false… …真正的初始化指派是在初始化階段進行的。同時要注意如果類變量被final修飾那麼準備結果的結果就會不同

public final static int value=123;      

編譯時Javac将會為value生成ConstantValue屬性,在準備階段虛拟機就會根據ConstantValue的設定将value指派為123.

解析

 解析階段就是将常量池内的符号引用替換為直接引用的過程。解析階段包含以下内容。

   類或接口的解析

   字段解析

   類方法解析

   接口方法解析

初始化

 在準備階段已經對類變量指派過一次了,當時是賦予的零值,而到了初始階段則會根據我們主觀計劃去初始化類變量和其他資源,其本質初始化階段是執行類構造器<clinit>方法的過程,在這個過程中有幾個要注意的地方

   靜态語句塊隻能通路到定義在靜态語句塊之前的變量。可以給定義在之後的變量指派但不可以通路

public class Test2 {

    static{
        i = 10;  // 能指派
        System.out.println(i); // 但不能通路,提示 非法向前引用
    }
    
    static int i = 0;
    
}      

   <clinit>不需要顯示的調用父類的類構造器。虛拟機保證子類的<clinit>方法執行之前父類的<clinit>方法已經執行

   由于父類先執行<clinit>方法,是以父類的靜态語句塊會優先于子類的靜态語句塊執行

public class Test2 {
    
    static class Parent{
        static int A = 1;
        static{
            A = 2;
        }
    }

    static class Sub extends Parent{
        static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}      

輸出結果是2而不是1.

   <clinit>方法對于類或接口來說并不是必需的。如果一個類中沒有靜态語句塊也沒有對變量的指派操作,那麼編譯器可以不為這個類生成<clinit>方法。

   接口中不能使用靜态語句塊,任然有變量指派操作,是以接口和類一樣也會生成<clinit>()方法,但接口和類不同,接口中的<clinit>()方法不需要先執行父接口的<clinit>()方法,隻有當父接口中定義的變量使用時父接口才會初始化。接口的實作類在初始化的時候一樣不會執行<clinit>方法

   同一個類隻會被加載一次,/()方法也隻會執行一次,如果多線程環境中隻會有一個線程執行<clinit>方法,其他線程需要等待其執行完成。如果執行比較耗時那麼會産生阻塞。

public class Test2 {

    static {
        if (true) {
            System.out.println(Thread.currentThread().getName()+"開始初始化...");
            while (true) {
                // 死循環 阻塞
            }
        }
    }
}      
/**
 * 測試
 * 
 * @author 波波烤鴨
 * @email [email protected]
 *
 */
public class Test {
    public static void main(String[] args) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "線程開始...");
                Test2 t = new Test2();
                System.out.println("線程結束...");

            }
        }).start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "線程開始...");
                Test2 t = new Test2();
                System.out.println("線程結束...");
            }
        }).start();
    }
}      

輸出結果

Thread-0線程開始...
Thread-1線程開始...
Thread-0開始初始化...      

一個線程在初始化,但死循環了,另一個線程隻能等待。

參考《深入了解java虛拟機》