JVM類加載過程學習總結

jopen 11年前發布 | 28K 次閱讀 JVM Java開發

JVM類加載過程學習總結

先不說JVM類加載的原理,先看實例:

NormalTest類,包含了一個靜態代碼塊,執行的任務就是打印一句話。

/**
 * 在正常類加載條件下,看靜態代碼塊是否會執行
 * @author jianying.wcj
 * @date 2013-6-21
 */
public class NormalTest {

    static {
        System.out.println("hello world!");
    }
}

TestStatic類, 有三行代碼,其中兩行被注釋,測試過程是,在執行其中任意一行代碼的時候,注釋掉其余兩行。

public class TestStatic {

    public static void main(String[] args) throws ClassNotFoundException {
        /**
         * 實驗1
         */
        Class.forName("NormalTest");
        /**
         * 實驗2
         */
        //NormalTest nt = new NormalTest();
        /**
         * 實驗3
         */
        //TestStatic.class.getClassLoader().loadClass("NormalTest");
    }
}

測試的輸出的結果是: 在執行 Class.forName("NormalTest")的時候,輸出了“Hello world!”,在執行NormalTest nt = new NormalTest();的時候也輸出了“Hello world!” 但是在執行代碼TestStatic.class.getClassLoader().loadClass("NormalTest");卻 沒有輸出“Hello world!” 下面分析一下原因或者說看看這三行代碼的內部實現的異同。 以上三行代碼其實在執行的時候都會去加載NormalTest.class,這里可以不準確的說以上三行代碼是三種加載類的方式。從實驗的輸出來看,可以 確定實驗1 和實驗2 在加載NormalTest的時候執行了靜態代碼塊,而實驗3 直接調用ClassLoader來loadclass的時候沒有執行靜態代碼塊。執行靜態代碼塊的過程其實就是初始化類的過程,話說到這,說白了,前兩種 方式加載類的時候對類進行了初始化,而第三種沒有,那么看看部分代碼的實現。

 public static Class forName(String className)  throws ClassNotFoundException {
     return forName0(className, true, ClassLoader.getCallerClassLoader());
}

上面這段代碼是Class.forName()的定義,實現里直接調用了forName0(),forName0的方法簽名是:

 private static native Class forName0(String name, boolean initialize,
                                ClassLoader loader)throws ClassNotFoundException;

這是個本地方法,本地方法的C++實現先不研究(拋磚引玉一下,誰有好的研究可以分享一下),這個本地方法第二個參數是initialize,這個參數的 true或false就是告訴虛擬機,在根據類的全限定名name加載類的時候,要對類進行初始化。在forName調用forName0的時候,看以看 到initialize設置成了true,所以我們的類的靜態代碼塊就被執行了(類被初始化了)。 實驗2在new一個對象的時候,也會在加載類的時候觸發其初始化方法,這個的實現在虛擬機實現的C++代碼里,JVM虛擬機規范指出,在執行new指令創 建一個對象的時候要對加載的類進行初始化(《深入理解java虛擬機》第七章有說)。實驗3在執行ClassLoader.load一個class的時候 屬于被動加載類,根據虛擬機規范不會對類進行初始化。對于new和ClassLoader.load加載類的方式,在java代碼層面已經看不到是否需要 對類進行初始化的標志了,內部實現在JVM的C++實現中(C++實現邏輯待哥們的水平提高提高再做分析總結)。

上面都是根據實例的總結,下面來點官方的資料學習總結。

  1. 首先看下關于類加載的時候是否初始化的虛擬機規范:

    虛擬機規范則是嚴格規定了有且只有四種情況必須立即對類進行初始化(而加載、驗證、準備自然需要在此之前開始):

    1) 遇到new 、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行初始化,則需要先觸發其初始化。生成這4條指令 的最常見的java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字 段除外)的時候,以及調用一個類的靜態方法的時候。

    2) 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

    3) 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

    4) 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

    可見:當滿足上述4中條件之一的任何一種情況都會執行類的靜態代碼塊,而除上述4中情況外,則不會對類的初始化(注意加粗的有且只有4個字)。

2.類從加載到卸載,整個過程可以描述為7個階段:JVM類加載過程學習總結

加載:虛擬機需要完成以下三件事情:

  • a) 通過一個類的權限定名來獲取此類定義的二進制字節流。
  • b) 將這個字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
  • c) 在java堆中生成一個代表這個類的java.lang.Class對象,作為訪問方法區的入口

驗證:

  • 連接階段的第一步,這一階段的目的是為了確保 Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。(備注:雖然在java語言是相對安全的,但是在字節碼層面, 上述java代碼無法做到的事情都是可以實現的,至少在語義上是可以表達出來的。所以對字節流進行驗證是相當必要的)

準備:

  • 準備階段是正式為變量分配內存并設置變量初始值的階段,這些內存都將在方法區中進行分配。

解析:

  • 解析過程就是在類型的常量池中尋找類、接口、字段和方法的符號引用,把這些符號引用替換成直接引用的過程。(至于什么是符號引用,什么是直接引用,參考這篇博客:http://blog.csdn.net/lantian0802/article/details/9152657

(ps:驗證、準備、解析統稱為連接)

初始化:

  • 為類的靜態變量賦予正確的初始值,當然也包括執行靜態代碼塊的內容。

ps: 直接引用和符號引用的一個生動的例子: 那么什么是符號引用,什么又是直接引用呢?我們來舉個例子:我們要找一個人,我們現有的信息是這個人的身份證號是1234567890。只有這個信息我們 顯然找不到這個人,但是通過公安局的身份系統,我們輸入1234567890這個號之后,就會得到它的全部信息:比如湖北省武漢市武漢大學張三,通過這個 信息我們就能找到這個人了。這里,123456790就好比是一個符號引用,而湖北省武漢市武漢大學張三就是直接引用。在內存中也是一樣,比如我們要在內 存中找一個類里面的一個叫做show的方法,顯然是找不到。但是在解析階段,jvm就會把show這個名字轉換為指向方法區的的一塊內存地址,比如 c17164,通過c17164就可以找到show這個方法具體分配在內存的哪一個區域了。這里show就是符號引用,而c17164就是直接引用。在解 析階段,jvm會將所有的類或接口名、字段名、方法名轉換為具體的內存地址。

來自:http://blog.csdn.net/lantian0802/article/details/9152699

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!