Java-類加載(類的生命周期)


類從被加載到虛擬機內存開始,到卸載出內存為止。

解析階段在某些情況下可以在初始化后再開始,這是為了支持 Java 語言的運行時綁定。

 

一、類加載時機

JVM 規范沒有強制約束類加載過程的第一階段(加載)什么時候開始,但對於“初始化”階段,有着嚴格的規定。

 

1.1.有且僅有 5 種情況必須立即對類進行“初始化”:

1.在遇到 new、putstatic、getstatic、invokestatic 字節碼指令時,如果類尚未初始化,則需要先觸發初始化。
2.對類進行反射調用時,如果類還沒有初始化,則需要先觸發初始化。
3.初始化一個類時,如果其父類還沒有初始化,則需要先初始化父類。
4.虛擬機啟動時,用於需要指定一個包含 main() 方法的主類,虛擬機會先初始化這個主類。
5.當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結果為 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類還沒初始化,則需要先觸發初始化。

這 5 種場景中的行為稱為對一個類進行主動引用,除此之外,其它所有引用類的方式都不會觸發初始化,稱為被動引用

 

1.2.幾種被動引用:

1.通過子類引用父類的靜態字段,不會導致子類初始化。對於靜態字段,只有直接定義這個字段的類才會被初始化。

class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
        // SuperClass init!
    }
}
View Code

2.通過數組定義來引用類,不會觸發此類的初始化。

class SuperClass2 {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 123;
}

public class NotInitialization2 {
    public static void main(String[] args) {
        SuperClass2[] superClasses = new SuperClass2[10];
    }
}
View Code

3.常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLO_BINGO = "Hello Bingo";
}

public class NotInitialization3 {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO_BINGO);
    }
}
View Code

編譯通過之后,常量存儲到 NotInitialization 類的常量池中,NotInitialization 的 Class 文件中並沒有 ConstClass 類的符號引用入口,這兩個類在編譯成 Class 之后就沒有任何聯系了。

 

1.3.關於接口加載

當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,當真正用到父接口的時候才會初始化。

 

 

二、類的加載過程

 2.1.加載

JVM 需要完成 3 件事:

1.通過類的全限定名獲取該類的二進制字節流。
2.將二進制字節流所代表的靜態結構轉化為方法區的運行時數據結構。
3.在內存中創建一個代表該類的 java.lang.Class 對象,作為方法區這個類的各種數據的訪問入口。

怎樣獲取類的二進制字節流,JVM 沒有限制。除了從編譯好的 .class 文件中讀取,還有以下幾種方式:

從 zip 包中讀取,如 jar、war 等
從網絡中獲取
通過動態代理生成代理類的二進制字節流
從數據庫中讀取
。。。

數組類本身不通過類加載器創建,由 JVM 直接創建,再由類加載器創建數組中的元素類。

加載階段與連接階段的部分內容交叉進行,但這兩個階段的開始仍然保持先后順序。

 

2.2.驗證

確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

 

2.3.准備

為類變量(靜態成員變量)分配內存並設置初始值的階段。這些變量(不包括實例變量)所使用的內存都在方法區中進行分配。

基本類型初始值(JDK8)https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.5

對於 byte 類型,默認值為零,即(byte)0。
對於 short 類型,默認值為零,即(short)0。
對於 int 類型,默認值為零,即 0。
對於 long 類型,默認值為零,即 0L。
對於 float 類型,默認值為正零,即 0.0f。
對於 double 類型,默認值為正零,即 0.0d。
對於 char 類型,默認值為空字符,即 '\u0000'。
對於 boolean 類型,默認值為 false。
對於所有引用類型,默認值為 null。

存在特殊情況 https://www.jianshu.com/p/520295a63967

/**
 * 准備階段過后的初始值為 0 而不是 123,這時候尚未開始執行任何 Java 方法
 */
public static int value = 123;

/**
 * 同時使用 final 、static 來修飾的變量(常量),並且這個變量的數據類型是基本類型或者 String 類型,就生成 ConstantValue 屬性來進行初始化。
 * 沒有 final 修飾或者並非基本類型及 String 類型,則選擇在 <clinit> 方法中進行初始化。
 * 准備階段虛擬機會根據 ConstantValue 的設置將 value 賦值為 123
 */
public static final int value = 123;
View Code

 

2.4.解析

虛擬機將常量池內的符號引用替換為直接引用。會把該類所引用的其他類全部加載進來( 引用方式:繼承、實現接口、域變量、方法定義、方法中定義的本地變量)

https://www.cnblogs.com/shinubi/articles/6116993.html

符號引用:一個 java 文件會編譯成一個class文件。在編譯時,java 類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。

直接引用:直接指向目標的指針(指向方法區,Class 對象)、指向相對偏移量(指向堆區,Class 實例對象)或指向能間接定位到目標的句柄。

 

2.5.初始化

類加載過程的最后一步,是執行類構造器 <clinit>() 方法的過程。

<init>()  與 <clinit>() 介紹:  https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9

https://blog.csdn.net/u013309870/article/details/72975536

<init>():為 Class 類實例構造器,對非靜態變量解析初始化,一個類構造器對應個。

<clinit>():為 Class 類構造器對靜態變量,靜態代碼塊進行初始化,通常一個類對應一個,不帶參數,且是 void 返回。當一個類沒有靜態語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生成 <clinit>() 方法

加載順序:

<clinit>() 方法是由編譯器自動收集類中的所有類變量的賦值動作語句靜態塊(static {})中的語句合並產生的,編譯器收集的順序由語句在源文件中出現的順序所決定。

靜態語句塊中只能訪問定義在靜態語句塊之前的變量,定義在它之后的變量,在前面的靜態語句塊中可以賦值,但不能訪問。

static {
    i = 0;  // 給后面的變量賦值,可以正常編譯通過
    System.out.println(i);  // 使用后面的變量,編譯器會提示“非法向前引用”
}
static int i = 1;

虛擬機會保證在子類的 <clinit>() 方法執行之前,父類的 <clinit>() 方法已經執行完畢。

由於父類的 <clinit>() 方法先執行,意味着父類中定義的靜態語句塊要優先於子類的變量賦值操作。

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

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

public static void main(String[] args) {
    System.out.println(Sub.B);  // 輸出 1
}
View Code

來看一個類屬性加載順序的問題

public class JvmTest {

    public static JvmTest jt = new JvmTest();

    public static int a;
    public static int b = 0;

    static {
        a++;
        b++;
    }

    public JvmTest() {
        a++;
        b++;
    }

    public static void main(String[] args) {
        /**
         * 准備階段:為 jt、a、b 分配內存並賦初始值 jt=null、a=0、b=0
         * 解析階段:將 jt 指向內存中的地址
         * 初始化:jt 代碼位置在最前面,這時候 a=1、b=1
         *          a 沒有默認值,不執行,a還是1,b 有默認值,b賦值為0
         *          靜態塊過后,a=2、b=1
         */
        System.out.println(a);  // 輸出 2
        System.out.println(b);  // 輸出 1
    }
}
View Code

關於接口初始化:

接口中不能使用靜態代碼塊,但接口也需要通過 <clinit>() 方法為接口中定義的靜態成員變量顯式初始化。

接口與類不同,接口的 <clinit>() 方法不需要先執行父類的 <clinit>() 方法,只有當父接口中定義的變量被使用時,父接口才會初始化。

 

虛擬機會保證一個類的 <clinit>() 方法在多線程環境中被正確加鎖、同步。如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的 <clinit>() 方法。


https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html

https://github.com/doocs/jvm/blob/master/docs/08-load-class-time.md

https://github.com/doocs/jvm/blob/master/docs/09-load-class-process.md


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM