深入Java類加載全流程,值得你收藏


先測試一番,全對的就走人

//題目一
class Parent1{
    public static String parent1 = "hello parent1";
    static { System.out.println("Parent1 靜態代碼塊"); }
}
class Children1 extends Parent1{
    public static String children1 = "hello children1";
    static {System.out.println("Children1 靜態代碼塊");}
}
//----------------------------------------------------------------
//題目二
class GrandParent2{
    static { System.out.println("GrandParent2靜態代碼塊"); }
}
class Parent2 extends GrandParent2{
    public static String parent2="hello parent2";
    static{ System.out.println("Parent2 靜態代碼塊");}
}
class Children2 extends Parent2{
    public static String children2 ="hello children2";
    static{ System.out.println("Children2 靜態代碼塊");}
}
//----------------------------------------------------------------
//題目三
class GrandParent3{
    static { System.out.println("GrandParent3靜態代碼塊"); }
}
class Parent3 extends GrandParent3{
    public final static String parent3="hello parent3";
    static{ System.out.println("Parent3 靜態代碼塊");}
}
class Children3 extends Parent3{
    public static String children3 ="hello children3";
    static{ System.out.println("Children3 靜態代碼塊");}
}
//測試
public class ClassLoaderTest {
    public static void main(String[] args) {
        //測試一的輸出
        System.out.println(Children1.children1);
        System.out.println("-------------------------------");
        //測試二的輸出
        System.out.println(Children2.parent2);
        System.out.println("--------------------------------");
        //測試三的輸出
        System.out.println(Children3.parent3);
    }
    //你認為輸出什么呢
}
答案如下

Parent1 靜態代碼塊
Children1 靜態代碼塊
hello children1


GrandParent2靜態代碼塊
Parent2 靜態代碼塊
hello parent2


hello parent3

如果看清到這里,你的回答和結果一致,那么你真的懂了,可以轉載給他人了,如果出乎你的意料,請認真看完。

什么是類加載(或者初始化)

Java源代碼經過編譯之后轉換成class文件,在系統運行期間當需要某個類的時候,如果內存中還沒該class文件,那么JVM需要對這個類的class文件進行加載,連接,初始化,JVM通常會連續完成這三步,這個過程叫做類的加載或者初始化, 類從磁盤加載到內存必須經歷這三個階段的。

重點是:類的加載都是在程序運行期間完成的,這提供了無限可能,意味着你可以在某個階段對類的字節碼進行修改,JVM也確實提供了這樣的功能。

類的加載並不是對象的創建,類的加載是在為對象創建前做一些信息准備。

類的生命周期

我們明白了什么是類的加載,那么從類的加載到最后類的卸載成為類在JVM中的聲明周期,這個生命周期總共包含了七個階段:我畫一張圖,如下,我們逐個分析一下類的生命周期的每一步。

這是類的生命周期的,但它不總是按照這個固定的流程進行的,我們先知道這個就行,后面再說。

加載

類的加載指的是把class文件從磁盤讀入內存中,將其放入元數據區域並且創建一個Class對象,放入堆中,Class對象是類加載的最終產品,Class對象並不是new出來的對象。

元數據區域存儲的信息

  1. 這個類型的完整有效名
  2. 這個類型的直接父類完整有效名
  3. 這個類型的修飾符(public final abstract等)
  4. 這個類型的直接接口的列表

Class對象中包含的如下信息,這也是我們能夠通過Class對象獲取類的很多信息的原因

  1. 類的方法代碼,方法名,字段等
  2. 類的返回值
  3. 類的訪問權限

加載class文件有很多種方式,可以從磁盤上讀取,可以從網絡上讀取,可以從zip等歸檔文件中讀取,可以從數據庫中讀取

驗證

驗證的目的是驗證class文件的正確性,是否能夠被當前JVM虛擬機執行,主要包含了一些部分驗證,驗證非常重要,但不是必須的(正常情況下都是正確的)
文件格式驗證:比如JDK8加載的是JDK6下編譯的class文件,這肯定不行。
元數據驗證:確保字節碼描述信息符合Java語言規范的要求,你理解為校驗外殼,比如類中是否實現了接口的所有方法。
字節碼驗證:確定程序語義執行是合法的,校驗內在,校驗方法體,防止字節碼執行過程中危害JVM虛擬機。
符合引用驗證:其對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗,比如:符號引用中的類、字段、方法的訪問性是否可被當前類訪問,通過全限定名,是否能找到對應的類。

准備(重點)

驗證完成之后,JVM就開始為類變量(靜態變量) 分配內存,設置初始化值, 記住兩點

  1. 不會為成員變量分配內存的。
  2. 初始化值是指JVM默認的指,不是程序中指定的值。

看如下代碼,你就明白了:

//類變量,初始化值是 null, 不是123
public static String s1 = "123"
//成員變量
public String s2 = "456"

但有一個特殊,如果一個類變量是final修飾的常量,那么在准備階段就會被賦值為程序中指定的值,如下代碼,初始值是123

//初始值是123,不是null
public static final String s1 = "123"

為什么會這樣呢?兩行代碼的區別在於final,final在Java中代表着不可變,不能賦值了之后重新賦值,所以一開始就必須賦值為用戶想要的默認值,而不是Java語言的默認值。而不是final修時的變量有可能在之后發生變化,所以就先賦值為Java語言的默認值。

解析

解析階段主要是將常量池中的符號引用轉換為直接引用,解析動作主要包含類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用。

符號引用包括什么呢?

  1. 類和方法的全限定名
  2. 字段的名稱和描述符
  3. 方法的名稱和描述符,

直接引用是是什么呢?一個指向目標的指針地址或者句柄。
舉個例子如下:

// 123 是一個符號引用,123所對應的內存中的地址是一個直接引用。
public static final String s1 = "123"

常量池是什么呢?,常量池包含好多種,字符串常量池,class常量池,運行時常量池,這里指的是class常量池。我們寫的每一個Java類被編譯后,就會形成一份class文件,class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池,用於存放編譯器生成的各種字面量和符號引用,每個class文件都有一個class常量池。

比如解析階段,找不到某個字段就拋出NoSuchFieldError,同理NoSuchMethodError

初始化(重點)

初始化階段用戶定義的Java代碼才會真正開始執行,一般來說當首次主動使用某個類的時候就會對該類初始化,初始化某個類時也會初始化這個類的父類,這里的首次主動使用,大家要理解清楚了,第二次使用時不會初始化的。類的初始化其實就是執行類構造器的過程,這個不是我們代碼定義的構造方法。

下面列舉了JVM初始化類的時機:

  1. 創建對象時(比如:new Person())
  2. 訪問類變量時
  3. 調用類的靜態方法時
  4. 反射加載某個類是(Class.forName("....."))
  5. Java虛擬機啟動時被標明為啟動類的類(單測時),Main方法的類。

初始化時類變量會被賦予真正的值,也就是開發人員在代碼中定義的值,也會執行靜態代碼塊。

JVM初始化類的步驟:

  1. 若該類還沒有被加載和連接,則程序先加載並連接該類
  2. 若該類的父類還沒有初始化,則先初始化該類的夫類
  3. 若該類中有靜態代碼塊,則系統依次執行這些代碼塊

上面提到了首次主動使用時初始化類,那么就有被動使用,被動使用是什么意思呢?比如說通過子類引用父類的靜態字段,那么子類會初始化嗎?答案是不會的,所以下面測試的子類的靜態代碼塊是不會執行的。

class Parent4{
    public final static String parent4="hello parent4";
}

class Children4 extends Parent4{
    static{ System.out.println("Children4 靜態代碼塊");}
}
public class ClassLoaderTest {
    public static void main(String[] args) {
        //測試四的輸出
        System.out.println(Children4.parent4);
    }
}

再說一個點解析時有提到常量池的概念,在經過初始化后,類就被加載到內存中去了,這個時候jvm就會將class常量池中的內容存放到運行時常量池中,運行時常量池也是每個類都有一個。在解析階段,會把符號引用替換為直接引用,解析的過程會去查詢字符串常量池,以保證運行時常量池所引用的字符串與字符串常量池中是一致的

上面還有一個關鍵字一般來說,那么不一般呢?類加載器並不需要等到某個類被首次主動使用時再加載它,JVM規范允許類加載器在預料某個類將要被使用時就預先加載它.

使用

使用就比較簡單了,JVM初始化完成后,就開始按照順尋執行用戶代碼了。

卸載

類卸載有個前提,就是class的引用是空的,要么程序中手動置為空,要么進程退出時JVM銷毀class對象,然后JVM退出。只要class引用不存在,那么這個類就可以回收了。

你自己可以試驗一下,寫一個classload類加載器,寫一個Test測試類,實際測試一下,我的測試代碼如下:

public class ClassTest {
    public static void main(String[] args){
        ClassLoaderMy classLoader = new ClassLoaderMy();
        classLoader.setRoot("D:\\github\\java_common\\target\\classes\\");
        Class clazz = classLoader.findClass("jvm.Test類中有一個靜態代碼塊。");
        Object obj = clazz.newInstance();
        System.out.println("1:"+clazz.hashCode());
        obj=null;
        System.out.println("2:"+clazz.hashCode());
        classLoader = null;
        System.out.println("3:"+clazz.hashCode());
        clazz = null;

        System.out.println("此時 obj classloader clazz 都為空了");

        classLoader = new ClassLoaderMy();
        classLoader.setRoot("D:\\github\\java_common\\target\\classes\\");
        clazz = classLoader.findClass("jvm.Test");
        System.out.println("4:"+clazz.hashCode());
        obj = clazz.newInstance();
    }
    //打印結果如下,看之前你猜一猜。Test類中有一個靜態代碼塊。
}

初始化了
1:1775282465
2:1775282465
3:1775282465
此時 obj classloader clazz 都為空了
4:1267032364
初始化了

最終結果你會發現,前三個hashcode的值是一樣的,第四個的值發生了變化,說明class文件被卸載了后重新加載生成了新的class對象,否則,同一個對象的hashcode是不會發生變化的,而且Test類的靜態代碼塊執行了兩遍,完整代碼地址如下:

https://github.com/sunpengwei1992/java_common/tree/master/src/jvm

我畫了一張圖,方便大家更好的理解,如下,當左邊的三個變量都指向為null時,最右邊的元數據區域的代表Class對象的Test二進制數據就會被卸載,當下次使用時就會被重新加載,初始化等。

但是,注意了 由JVM自帶的類加載器加載的類,在JVM生命周期中,始終不會被卸載,
JVM自帶的類加載器包括根類加載器,擴展類加載器,系統類加載器,這些回頭單聊。

解密測試題目

接下來我們聊一聊一開始的測試題,其實看到這里,想必大家都明白了吧,還是說一說。

第一個不用講了,都會。

第二題:子類Children2,父類Parent2, 祖父類GrandParent2,我們通過Chidlren2打印父類Parent2的靜態變量,類加載時,發現有父類存在,逐層往上加載,那么Parent2和GrandParent2都會被加載,所以Parent2和GrandParent2的靜態代碼塊都會被執行,而Children2就不會被加載了,因為不符合首次主動使用的條件。

第三題:同樣的道理,只是Parent3和GrandParent3的靜態代碼塊為什么沒執行呢,因為Parent3的靜態變量是final類型的,在准備階段就已經完成了,不需要再逐層往上加載了.

提一下接口的加載

當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,當真正用到父接口的時候才會加載該接口,如下代碼,執行main方法,Parent5接口是不會被加載的,parent5變量也是不會被初始化的。

interface Parent5{
    public final static  String parent5 = "hello parent5";
}
interface Children5 extends Parent5{
    public final static String children5 = "hello children5";
}
public static void main(String[] args) {
    System.out.println(Children5.children5);
}

表格整理一下流程


免責聲明!

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



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