類加載都經歷了啥

前言

前面介紹了字節碼的讀法,下面就是把字節碼存入到內存中,那麼他又是怎麼加載的這些字節碼文件的呢?java

大綱

什麼是類加載

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型,這就是虛擬機的類加載機制數據結構

咱們從上面的定義能夠看出來他要經歷 加載到內存-->數據校驗-->解析-->初始化多線程

固然上面的過程並不詳細,詳細的過程以下:函數

類的生命週期總共細分爲7個階段

  • 加載
  • 驗證
  • 準備
  • 解析
  • 初始化(這個步驟後面不歸類加載)
  • 使用
  • 卸載

個人全部文章同步更新與Github--Java-Notes,想了解JVM,HashMap源碼分析,spring相關能夠點個star,劍指offer題解(Java版),設計模式。能夠看個人github主頁,天天都在更新喲。

邀請您跟我一同完成 repo


加載

加載過程是第一步,他須要完成三個步驟

  • 經過一個類的全限定名來獲取定義此類的二進制字節流
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
  • 內存中生成一個表明這個類的 java.lang.Class 對象,做爲方法區這個類的各類數據的訪問接口

加載這個過程的實現多種多樣,虛擬機規範也並無對其進行明確的約束,好比第一句 "經過全限定名獲取二進制流"。沒有規定用設麼方法獲取,從哪裏獲取。

那麼就出現了多種多樣的實現方式

  • 從zip中獲取,成爲了往後 war,jar等格式的基礎
  • 從網絡中獲取,典型應用 Applet
  • 由其餘文件生成,JSP應用
  • 等等…….

數組類

對於一個數組類型和一個非數組類型,後者的可控性在這個階段對於開發人員更強,

  • 由於咱們可使用系統提供的引導類加載器,

  • 也可使用本身定義的類加載器來完成(重寫 loadClass完成),

非數組類

  • 自己不禁類加載器建立,由虛擬機直接建立
  • 可是他的元素類型(去掉全部維度,去掉全部[]int[]是int,int[][]是int)是由類加載器建立
  • 組件類型是去掉一維的類型,好比int[][]int[]

規則以下:

  • 組件是引用類型
    • 那就遞歸前面定義的加載過程去加載這個組件類型
    • 可見性:和組件類型一致
  • 若是不是引用類型(如int[]的組件類型是基本類型)
    • 與引導類加載器相關聯
    • 可見性:默認爲public

加載過程可能和驗證階段的一部分交叉進行,可是兩個的開始時間還是保持固定的先後順序

驗證(非必要)

這個過程不必定非要進行,若是已經反覆驗證過,實施階段能夠經過 -Xverify:none 來關閉來進行優化。

這個階段主要是保護虛擬機不會由於載入有害的字節流而崩潰

主要完成下列四個階段的驗證:

  • 文件格式驗證

    • 是否以魔數 0xCAFEBABE開頭
    • 主次版本號是否在當前虛擬機處理範圍內
    • 常量池的常量中是否有不被支持的常量類型(檢查常量 tag標誌)
    • ……..

    只有經過了這個階段,字節碼才載入到內存中,後面的3個驗證再也不對字節碼進行操做,直接操做方法區的存儲結構

  • 元數據驗證

    • 是否有父類,(除Object類,都有父類)
    • 是否繼承了不能被繼承的類(如被final修飾)
    • 若是這個類不是抽象類,是否實現了其父類或接口中要求實現的全部方法
    • …….
  • 字節碼驗證

    主要是確保被校驗的方法在運行時不會作出危害虛擬機的事件

    • 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,如不會出現相似這樣的操做,操做棧中放了一個int 類型的數據,可是使用時卻使用long類型進行載入到本地變量表
    • 保證跳轉指令不會跳轉到方法體之外的字節碼指令上
    • …….
  • 符號引用驗證

    符號引用轉化爲直接引用,這個轉化動做將發生在解析階段。主要校驗下列內容

    • 符號引用中經過字符串描述的全限定名是否能找到對應的類
    • 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段
    • …….

    若是不能經過驗證,會拋出java.lang.IncompatibleClassChangeError異常類型的子類,如

    • Java.lang.IllegalAccessError
    • java.lang.NoSuchFieldError
    • Java.lang.NoSuchMethodError

準備

準備階段是正式爲類變量分配內存並設置初始值的階段,這些變量所使用的內存在方法區中分配

這裏有兩個注意點;

  • 這裏是類變量(被static修飾的變量)不是實例變量

  • 設置初始值並不等於初始化,只是將其在內存中的值設置爲"零值"

    ​ 例如 public static int value = 123;

    ​ 準備階段完成以後是value 是0而不是123;

    由於這個時候還沒有執行任何Java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器<clinit>()方法中,因此這個會在初始化階段執行。初始化階段會講到

  • 若是是被final修飾的類變量,那麼準備階段完成以後他就是123

    • public static final int value = 123;

    由於被final修飾的值是存放在 字節碼文件的 ConstantValue屬性表中,若是不瞭解這個能夠看看個人這篇文章,能看懂的字節碼

    各個數據類型的初始值以下表

解析

解析是虛擬機將常量池內的符號引用轉換爲直接引用的過程

兩者異同

  • 符號引用
    • 符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可
    • 符號引用與內存佈局無關,引用的目標並不必定已經加載到內存中
  • 直接引用
    • 直接引用是能夠直接指向目標的指針相對偏移量或是一個能間接定位到目標的句柄
    • 直接引用和虛擬機佈局相關,若是有直接引用,那麼引用的目標必定存在內存中

初始化(重要)

初始化相對來講比較重要,由於他是類加載的最後一步,也是開始真正執行類中定義的Java代碼,前面的步驟除了能夠本身定義類加載器以外都是由虛擬機主導或者控制的

初始化的時機(有且只有)

主動引用:

  • 遇到newgetstaticputstaticinvokestatic這四條命令的時候,若是類沒有進行初始化,則進行初始化
  • 使用 java.lang.reflect包的方法對類進行反射調用的時候,若是沒有,則初始化
  • 當初始化一個類時,若是發現父類沒有進行初始化,則須要先觸發其父類的初始化
  • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機先初始化這個主類
  • 當使用 JDK1.7之後的動態語言支持時,若是一個 java.lang.invoke.MethodHandle實例最後的解析結果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行初始化,則先觸發初始化

這5類場景被稱爲主動引用,除此以外,全部引用類的方法都不會觸發初始化,成爲被動引用

《深刻理解Java虛擬機》這本書,這段話放在了前面,不過我以爲這個應該放在後面,由於沒有以前的準備階段,咱們並不知道初始化階段是執行類構造器<clinit>()。因此他舉得例子有些就看不懂,爲啥要用static代碼塊

<clinit>()包含的內容、執行順序等等,這些也須要先了解

Clinit()方法

  • 內容
    • 他是由編譯器自動收集類中的全部類變量的賦值動做靜態語句塊(static{})
  • 執行順序
    • 編譯器的收集順序由語句在源文件中出現順序決定
    • 靜態語句塊中只能訪問到定義在靜態語句塊以前的變量
    • 和類的構造函數(實例構造器<init>()方法)不一樣,他不須要顯示的調用父類構造器,虛擬機會保證子類構造器方法執行前,父類構造器先執行。
      • 也就是說,父類的靜態代碼塊中的內容必定是先於類的變量賦值。後面會有示例
  • 類和接口的區別
    • <clinit>()方法對於類或接口來講並非必須的,若是一個類中沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法
    • 執行接口的clinit方法不須要先執行父類接口的<clinit>方法。只有當父接口中定義的變量使用時,父接口才被初始化(對比主動引用的第三條)
    • 接口的實現類在初始化時也不會執行接口的<clinit>()方法
  • 加鎖
    • 虛擬機會保證多線程環境下一個類的 <clinit>()會被正確的加鎖、同步,
    • 若是多線程同時初始化一個類,只有一個線程會去執行<clinit>()方法,其餘線程會被阻塞
    • 若是執行時間很長,那麼會形成線程阻塞

舉例

執行順序

package classInit;

/** * 由於父類初始化必定要在子類的<clinit>()方法前,因此輸出 2,參考執行順序的第三條 */
public class ClinitTest {
    static class Parent{
        public static int  A = 1;
        static {
            A = 2;
        }
    }
    static class Sub extends Parent{
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}
複製代碼

多線程阻塞

package classInit;

public  class DeadLockClass {
    static class DeadLoopClass {
        static {
            if (true) {
                System.out.println(Thread.currentThread() + "init DeadLockClass");
                while (true) {

                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = () -> {
            System.out.println(Thread.currentThread()+"start");
            DeadLoopClass dlc = new DeadLoopClass();
            System.out.println(Thread.currentThread()+"end");
        };


        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}
複製代碼

由執行結果可知,線程1已經進入阻塞;

被動引用的例子1(我把類所有放在一個代碼塊中)

package classInit.example1;

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

public class SubClass extends SuperClass{
    static {
        System.out.println("SubClassInit");
    }
}

/** * 不會觸發子類的初始化,由於他並無在那5個狀況中 * 子類引用父類的靜態字段,不會致使子類觸發 */
public class ClassInit {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

複製代碼

並無觸發子類的初始化,由於子類並不符合上述5中條件

package classInit.example2;

import classInit.example1.SuperClass;


/** * 經過數組定義來引用類,不會觸發此類的初始化 */
public class NotInit {
    public static void main(String[] args) {
        SuperClass[] superClasses = new SuperClass[10];
    }
}

複製代碼
package classInit.example3;

public class ConstClass {
    static {
        System.out.println("ConstClass init");
    }
    public static final String HELLOWORLD = "hello world";

}


/** * 沒有輸出 "ConstClass init", * 由於常量在編譯階段經過常量傳播優化,已經將常量放進了 NotInit 類的常量池中 * 之後NotInit對常量的引用實際都被轉化爲 NotInit 對自身常量池的引用 * * 也就是說實際上 NotInit 的class文件中並無ConstClass的符號引用入口 * 這兩個類在編譯成Class文件以後就已經不存在聯繫了 */
public class NotInit {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}
複製代碼

至此,類加載的過程已經所有結束

相關文章
相關標籤/搜索