Java JVM類加載機制解讀

1.什麼是類加載

首先你要知道一個類的從被加載到虛擬機內存中開始,到被初始化為止,是為類加載的整個過程。下圖就是類加載的整個過程:

在這裡插入圖片描述

一個類隻有經歷瞭加載、驗證、準備、解析、初始化這五個關卡才能被認為是實現瞭類加載。這,就是類加載。

註意一點:上面五個過程並不是按部就班地“完成”,而是按部就班地“執行”(除解析過程外)。執行時一定是先開始加載,再開始驗證,但加載過程中也可能會直接開始驗證。

2.類加載的過程

2.1加載

“加載”隻是是“類加載”過程的第一個階段,關於在什麼時候開始,規范並沒有進行強制約束,可以讓虛擬機自行把握。在這個階段中,Java虛擬機需要完成以下三件事:

1)通過一個類的全限定名來獲取這個類的二進制字節流

2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構

3)在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的方問入口

可以用一句話概括:加載是一個讀取Class文件,將其轉化為某種靜態數據結構存儲在方法區內,並在堆中生成一個便於用戶調用的java.lang.Class類型的對象的過程

2.2驗證

驗證是連接階段的第一步,這個階段的目的是確保Class文件的字節流中包含的信息符合約束要求,,保證這些信息被當做代碼運行後不會危害虛擬機自身的安全。

這一過程瞭解即可。

2.3準備

準備階段是正式為類中定義的變量(這裡說的是靜態變量,也就是被static修飾的變量)分配內存,並設置類變量初始值的階段。

這裡有兩點需要強調:

1)首先這裡進行內存分配的僅僅是類變量,而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。

2)其次這裡設置的初始值“通常情況”下是數據的零值,而不是用戶本身對它賦的初值。

如下代碼:

public static int a = 10;

變量a在準備階段後的初始值是0,而不是10,因為現在隻是在類加載過程中,還沒有執行任何方法。

上面說到“通常情況”,那就說明還有特殊情況咯,加修飾詞final時:

public static final int a = 10;

這時在準備階段虛擬機就會將a設置為10。其實也不難理解:我們將它設置為常量,那就肯定在任何時候都不能修改啊,天子犯法與庶民同罪!

2.4解析

解析階段是Java虛擬機將常量池內的符號引用替換為直接引用的過程,這一過程也可能在初始化後進行,並不一定和流程圖的執行順序一樣。

符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,隻要使用時能無歧義地定位到目標即可。

直接引用:直接引用是可以直接指向目標的指針、相對偏移量或者是一個能間接定位到目標的句柄。

這一過程比較復雜,有興趣可以參考《深入理解Java虛擬機》

2.5初始化【重中之重之重中重】

類的初始化階段是類加載過程的最後一個階段。在這個階段Java虛擬機才開始真正執行類中編寫的Java程序代碼。

初始化階段有以下六種情況必須立即對類進行“初始化”:

  • 1)使用new關鍵字實例化對象的時候
  • 2)讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候
  • 3)調用一個類的靜態方法的時候
  • 4)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 5)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 6)當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

光說不行,主要看

第一段代碼:

package com.bit.JVMTest;

class Father {

    public  static int a = 10;
    
    static {
        System.out.println("爸爸靜態代碼塊");
    }
}


class Son extends Father{

    public static int b = 20;
    
    static {
        System.out.println("兒子靜態代碼塊");
    }
}


public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println(Son.b);
    }
}

運行結果:
爸爸靜態代碼塊
兒子靜態代碼塊
20

首先Son.b是在讀取Son類自己的靜態字段,這點符合上面六中情況的第二種:讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候需要進行初始化。

其次Son類繼承Father類,也就符合第五條:當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,所以我們先初始化的應該是Father類,然後是Son類。

因此,打印的內容首先是爸爸靜態代碼塊(父類先初始化),然後是兒子靜態代碼塊(子類再初始化),最後是我們想要打印的b(20)本身。

再看

第二段代碼:

package com.bit.JVMTest;

class  grandFather{
    static{
        System.out.println("爺爺靜態代碼塊");
    }
}

class Father extends grandFather{

    public  static int a = 10;
    
    static {
        System.out.println("爸爸靜態代碼塊");
    }
}


class Son extends Father{

    public static int b = 20;
    
    static {
        System.out.println("兒子靜態代碼塊");
    }
}


public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println(Son.a);
    }
}

運行結果:
爺爺靜態代碼塊
爸爸靜態代碼塊
10

首先要明確:Son.a是在讀取父類Father類的靜態字段(註意a字段在Son類的父類中),而不是讀取Son類本身的靜態字段

因此這次不會初始化Son類本身。

因此這次不會初始化Son類本身。

因此這次不會初始化Son類本身。

其它的和第一段代碼很相似:JVM在初始化Father類的時候,發現這個類還有一個父類沒有被初始化,那就先初始化它的父類:grandFather

因此,打印的內容首先是爺爺靜態代碼塊(Father類的父類先初始化),然後是爸爸靜態代碼塊(Father類再初始化),最後是我們想要打印的a(10)本身。

第三段代碼:

package com.bit.JVMTest;

class  grandFather{
    static{
        System.out.println("爺爺靜態代碼塊");
    }
}

class Father extends grandFather{

    public final static int a = 10;
    
    static {
        System.out.println("爸爸靜態代碼塊");
    }
}


class Son extends Father{

    public static int b = 20;
    
    static {
        System.out.println("兒子靜態代碼塊");
    }
}


public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println(Son.a);
    }
}

運行結果:10

看到這裡是不是想說臥**你*個*。

別急別急,這裡的主函數調用雖然和第二段代碼一樣,但是註意!!!我們給a這個靜態字段加瞭一個final修飾符

再看六條中的第(2)條:讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候會觸發類加載。

也就是說我們讀取的a是被final修飾的,讀取這種靜態字段並不會引起任何類的初始化,所以就直接打印a(10)瞭。

再看

最後一段代碼:

package com.bit.JVMTest;


class Father {

   public Father(){
       System.out.println("爸爸構造方法");
   }

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

    {
        System.out.println("爸爸普通代碼塊");
    }
}

class Son extends Father{
    public Son(){
        System.out.println("兒子構造方法");
    }

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

    {
        System.out.println("兒子普通代碼塊");
    }
}

public class ClassLoaderTest extends Son{
    public static void main(String[] args) {
        System.out.println("開始");
        new Son();//這裡實例化一個Son類的對象
        System.out.println("結束");
    }
}

運行結果:
 爸爸靜態代碼塊
 兒子靜態代碼塊
 開始
 爸爸普通代碼塊
 爸爸構造方法
 兒子普通代碼塊
 兒子構造方法
 結束

看到這裡是不是欲哭無淚,我**不學瞭我。別急先聽我細細分析一波~
這裡有一個細節:主類繼承瞭Son類!,這貌似沒什麼啊,但是還有一個細節:我們的main()方法是主類中的靜態方法!看到這裡是不是明白瞭些什麼?

沒錯!當我們調用main()方法的時候,就引起瞭主類的初始化,主類繼承Son類,Son類繼承Father類,所以就先進行Father類的初始化:打印爸爸靜態代碼塊,接著Son類初始化:打印兒子靜態代碼塊,最後該終於我主類初始化瞭:代碼中沒什麼可以初始化的…(尷尬)。

接下來是第二階段:執行main()方法:

1.先打印:開始字樣。

2.接著是構造 Son()實例,那麼就會先構造它的父類Father()的實例:構造實例時按照先執行代碼塊,再執行構造方法的順序來。所以就先打印瞭:爸爸普通代碼塊、爸爸構造方法 這幾個大字。然後再執行構造Son()的實例,構造順序一樣,所以就後打印瞭:兒子普通代碼塊、兒子構造方法 這幾個大字。

3.最後打印:結束字樣。

此時main()才方法真正結束。

總結

我們平常所說的類加載體現在代碼上就是初始化這一階段,我這裡結束的也僅限於此,想瞭解詳細的類加載可以參考《深入理解Java虛擬機》這本書,也可以看其他博主的知識總結。感謝你能看到這裡!

到此這篇關於Java JVM類加載機制解讀的文章就介紹到這瞭,更多相關Java JVM 類加載機制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: