JVM類加載機制分析小記

學海無涯,旅“途”漫漫,“途”中小記,如有錯誤,敬請指出,在此拜謝!

一、前情提要

今天小張童鞋發了我一個關於JVM類加載的題,絞盡腦汁未做對,研究研究,題目如下

package com.example.demo;

public class Test {
    public static int k = 0;
    public static Test t1 = new Test("t1");
    public static Test t2 = new Test("t2");
    public static int i = print("i");
    public static int n = 99;
    private int a = 0;
    public int j = print("j");

    {
        print("構造塊");
    }

    static {
        print("靜態塊");
    }

    public Test(String str) {
        System.out.println((++k) + ":" + str + "    i=" + i + "     n=" + n);
        ++i;
        ++n;
    }

    public static int print(String str) {
        System.out.println((++k) + ":" + str + "    i=" + i + "     n=" + n);
        ++n;
        return ++i;
    }

    public static void main(String args[]) {
        Test t = new Test("init");
    }
}

那麼見證奇蹟的時刻來了,輸出結果爲:

1:j    i=0     n=0
2:構造塊    i=1     n=1
3:t1    i=2     n=2
4:j    i=3     n=3
5:構造塊    i=4     n=4
6:t2    i=5     n=5
7:i    i=6     n=6
8:靜態塊    i=7     n=99
9:j    i=8     n=100
10:構造塊    i=9     n=101
11:init    i=10     n=102

不知道有多少小夥伴做對了呢?反正我只是做對了20%,不及格。

二、理論基礎

1、類加載生命週期

首先,我們需要銘記下面這個圖片,也就是jvm加載類的生命週期。所以一個類在被使用之前,經歷了加載->驗證->準備->解析->初始化五個過程(有的書中也會把驗證+準備+解析統一叫做連接)。
在這裏插入圖片描述
那麼,每個過程,jvm都對類做了哪些不可告人的事情呢?

1.1加載

在java程序運行之前,JVM會對類進行加載。在此過程中,JVM會把編譯完成的.class二進制文件加載到內存,後續提供程序使用,用到的就是類加載器ClassLoader 。加載階段與連接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未結束,連接階段就可能開始了。但是夾在加載階段進行的動作,仍然屬於連接階段的內容。

1.2連接-驗證

驗證是連接的第一步,目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危及虛擬機本身的安全。 驗證階段的四個步驟:文件格式檢驗、元數據檢驗、字節碼檢驗、符號引用檢驗。

文件格式檢驗:檢驗字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。
元數據檢驗:對字節碼描述的信息進行語義分析,以保證其描述的內容符合Java語言規範的要求。
字節碼檢驗:通過數據流和控制流分析,確定程序語義是合法、符合邏輯的。
符號引用檢驗:符號引用檢驗可以看作是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。

1.3連接-準備

該階段正式爲類變量分配內存並設置類變量初始值。這些變量所使用的內存將在方法區中進行分配。此時進行內存分配的僅包括類變量,而不包括實例變量(實例變量將會在對象實例化時隨着對象一起分配在Java堆中)。另外,在這裏分配的靜態類變量是將其值定義爲默認值。因爲在該階段並未執行任何Java方法,正確的賦值將在初始化階段執行。

1.4連接-解析

該階段虛擬機會將常量池內的符號引用替換爲直接引用的過程。

1.5初始化

這是類加載的最後一步,真正執行類中定義的字節碼,也就是.class文件。 初始化階段是執行類構造器方法的過程,以及真正初始化類變量和其他資源的過程。

2、名詞含義以及區別

2.1 構造代碼塊、構造函數代碼塊、靜態代碼塊區別

(1)構造代碼塊:直接在類中定義且沒有加static關鍵字的代碼塊稱爲{}構造代碼塊。構造代碼塊在創建對象時被調用,每次創建對象都會被調用,並且構造代碼塊的執行次序優先於類構造函數。比如:

{
    print("構造塊");
}

(2)構造函數:用於給對象進行初始化,是給與之對應的對象進行初始化,它具有針對性,函數中的一種。比如

public Test(String str) {...}

(3)靜態代碼塊:static{}包裹的代碼塊,且靜態代碼只執行一次,可以通過Class.forName(“classPath”)的方式喚醒代碼的static代碼塊,但是也執行一次。只執行一次的原因,百度了一下,大概是類被加載進內存中的方法區的時候調用靜態代碼塊,而加載類到內存中只需要執行一次即可,比如

static{
	System.out.println("static代碼塊");
}

(4)特點:
1:該函數的名稱和所在類的名稱相同。
2:不需要定義返回值類型。
3:該函數沒有具體的返回值。
(5)底層分析:通過反編譯可以看到,構造代碼塊中的代碼也是在構造方法中執行的。在編譯時的編譯器看來會默認將構造代碼塊中的代碼移動到構造方法中,並且移動到構造方法內容的前面。
(6)三者的順序
顯示static代碼初始化,然後是構造方法初始化,然後是構造函數初始化,並且靜態代碼只會初始化一次。比如測試方法

public class Test2 {
    public Test2() {
        System.out.println("HaHa:我是構造函數代碼塊");
    }

    {
        System.out.println("HeHe:我是構造代碼塊");
    }

    static {
        System.out.println("HoHo:我是靜態代碼塊");
    }

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

得到的結果爲

HoHo:我是靜態代碼塊
HeHe:我是構造代碼塊
HaHa:我是構造函數代碼塊
HeHe:我是構造代碼塊
HaHa:我是構造函數代碼塊

(7)加上繼承
假設Test3繼承上面的Test2,代碼如下

public class Test3 extends Test2 {
    public Test3() {
        System.out.println("Test3:HaHa:我是構造函數代碼塊");
    }

    {
        System.out.println("Test3:HeHe:我是構造代碼塊");
    }

    static {
        System.out.println("Test:3HoHo:我是靜態代碼塊");
    }

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

輸出結果爲

HoHo:我是靜態代碼塊
Test:3HoHo:我是靜態代碼塊
HeHe:我是構造代碼塊
HaHa:我是構造函數代碼塊
Test3:HeHe:我是構造代碼塊
Test3:HaHa:我是構造函數代碼塊
HeHe:我是構造代碼塊
HaHa:我是構造函數代碼塊
Test3:HeHe:我是構造代碼塊
Test3:HaHa:我是構造函數代碼塊

可以看出父類相同的類型,會在子類之前執行

三、問題分析

當方法執行時,先加載Test類,加載Test類時,會按順序加載靜態域(見四的解釋A),所以先執行

public static int k = 0;//1

然後執行

public static Test t1 = new Test("t1");//2

當執行該方法時,創建Test對象,就會運行Test類中的構造方法,即

public int j = print("j");//2.1

{
    print("構造塊");//2.2
}

public Test(String str) {...}//2.3(str爲t1)

然後執行

public static Test t1 = new Test("t2");//3

當執行該方法時,創建Test對象,就會運行Test類中的構造方法,即

public int j = print("j");//3.1

{
    print("構造塊");//3.2
}

public Test(String str) {...}//3.3(str爲t2)

然後執行

public static int i = print("i");//4

然後執行

public static int n = 99;//5

然後執行

static {
    print("靜態塊");//6
}

執行到此處,Test類的靜態域加載完畢,然後開始執行main函數中的代碼

public static void main(String args[]) {
    Test t = new Test("init");//7
}

當執行該方法時,創建Test對象,就會運行Test類中的構造方法,即

public int j = print("j");//7.1

{
    print("構造塊");//7.2
}

public Test(String str) {...}//7.3(str爲init)

四、解釋

解釋A

如果在main函數中增加輸出如下

 public static void main(String args[]) {
     System.out.println("haha");//增加此輸出
     Test t = new Test("init");
 }

則運行後的輸出爲

1:j    i=0     n=0
2:構造塊    i=1     n=1
3:t1    i=2     n=2
4:j    i=3     n=3
5:構造塊    i=4     n=4
6:t2    i=5     n=5
7:i    i=6     n=6
8:靜態塊    i=7     n=99
haha
9:j    i=8     n=100
10:構造塊    i=9     n=101
11:init    i=10     n=102

則可以看出,步驟1-8爲加載類的時候輸出的,9-11爲main函數輸出。

五、其他

參考文獻:
https://my.oschina.net/u/1458864/blog/2004785
https://baijiahao.baidu.com/s?id=1633972974070851508&wfr=spider&for=pc
https://yq.aliyun.com/articles/712207
http://www.sohu.com/a/225428891_819383
https://blog.csdn.net/hxhaaj/article/details/81174743
https://www.cnblogs.com/Heliner/p/10524699.html

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