一、類加載器深入解析與階段分解

一、類加載

1. 在Java代碼中,類型的加載,連接與初始化過程都是在程序運行階段完成的
2. 提供了強大的靈活性,增加了更多的可能性

二、類加載器深入剖析

1. Java虛擬機與程序的生命週期
2. 在如下情況下,Java虛擬機將結束生命週期
  • 執行了System.exit()方法
  • 程序正常運行結束
  • 程序在運行過程中遇到了異常或者錯誤而終止
  • 由於操作系統錯誤導致Java虛擬機進程終止

三、類的生命週期

在這裏插入圖片描述

1. 加載:查找並並加載類的二進制數據
2. 連接
3. 初始化:爲類的靜態變量賦予正確的初始值

將靜態變量的默認值替換爲正確的初始化值

4. 使用
5. 卸載

OSGi(開放服務網關協議,Open Service Gateway Initiative)技術是Java動態化模塊化系統的一系列規範

四、類的加載

  • 什麼是類的加載?

類的加載是指將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在內存中創建一個java.lang.Class對象(規範並未說明Class對象位於哪裏,HotSpot虛擬機將其放在了方法區中)用來封裝類在方法區內的數據結構

  • 方法區:jdk1.7及1.7之前jvm中都會存在方法區,1.8之後進行了改造,稱之爲meta space(元空間)
  • 加載.class文件的方式
  1. 從本地系統中直接加載(最常用)
  2. 通過網絡下載.class文件
  3. 從zip,jar等歸檔文件中加載.class文件
  4. 從專有數據庫中提取.class文件
  5. 將Java源文件動態編譯爲.class文件(動態代理的代理類在編譯期不存在,在運行期動態生成;或者servlet)
  • 類的加載最終產品就是位於內存中class對象
  • class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問改方法區內數據結構的接口
  • 有兩種類型的類加載器
  1. java虛擬機自帶的類加載器
  • 根類加載器(Bootstrap):沒有父加載器,負責加載Java的核心類庫
  • 擴展類加載器(Extension):父加載器爲根類加載器
  • 系統(應用)類加載器(System):也稱爲應用類加載器,父加載器爲擴展類加載器
  1. 用戶自定義的類加載器
  • java.lang.ClassLoader的子類
  • 用戶可以定製類的加載方式

在這裏插入圖片描述

  • 類加載器並不需要等到某個類“首次主動使用”時再加載它
  • JVM規範允許類加載器在預料某個類將要使用時預先加載加它,如果預先加載過程中遇到了.class文件缺失或者存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageErreor錯誤)
  • 如果這個類一直沒有被程序主動使用,類加載器則不會報告錯誤
  • 雙親委託機制

類加載器用來把類加載到Java虛擬機中,從JDK 1.2開始,類的加載採用雙親委託機制,這種機制能更好的保證Java平臺的安全;在雙親委託機制中,除了Java虛擬機自帶的根類加載器以外,其餘的類加載都有且只有一個父加載器

五、類的連接(驗證,準備,解析)

類被加載後,就進入連接階段。連接就是將已讀入到內存的類的二進制數據合併到虛擬機的運行時環境中去。

1. 驗證

  • 驗證:確保被加載的類的正確性

主要驗證字節碼文件是否符合Java虛擬機規範

  • 驗證內容(主要)
  • 類文件的結構檢測
  • 語義檢查
  • 字節碼驗證
  • 二進制兼容性驗證

2. 準備

  • 準備:爲類的靜態變量分配內存,並將其初始化爲默認值

準備階段,對象還未被創建,靜態變量相當於全局變量;初始化的時候會首先初始化爲該變量類型的默認值,而不是初始化爲給變量的賦值

3. 解析

  • 解析:把類中的符號引用轉爲直接引用

六、類的初始化

1. Java程序對類的使用分爲兩種
  1. 主動使用
  2. 被動使用
2. 類初始化的時機:首次主動使用的時候
  • 主動使用的情況(七種)
  1. 創建類的實例(最常見的通過new的方式)
  2. 訪問某個類或接口的靜態變量(取值),或者對該靜態變量賦值(賦值)
  3. 調用類的靜態方法(本質同第2種,可以利用javap反編譯class文件查看jvm助記符)
  4. 反射(如Class.forName(“com.lizza.Test”))
  5. 初始化一個類的子類(當初始化子類的時候必然會初始化所有的父類)
  6. java虛擬機啓動時被表明爲啓動類的的類(Java Test)
  7. JDK 1.7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的結果REF_getStatic,REF_putStatic,REF_invokeStatic句柄對應的類沒有初始化,則初始化(瞭解)
  • 注意:除了以上7種情況,其他使用Java類的方式都被看做是對類的被動使用,都不會導致類的初始化
3. 類初始化的步驟
  1. 假如這個類還沒有被加載和連接,則先進行加載和連接
  2. 假如這個類存在直接父類,並且這個父類還沒有初始化,則先初始化直接父類
  3. 假如類中存在初始化語句,則依次執行這些初始化語句
  • 注意:當Java虛擬機初始化一個類的時候,要求它的所有父類都已經被初始化,但是這條規則並不適用於接口
  • 初始化一個類的時候,並不會先初始化它所實現的接口
  • 初始化一個接口時,並不會先初始化它的父接口

總結:因此一個父接口並不會因爲它的子接口或實現類的初始化而初始化,只有當程序首次使用特定的接口的靜態變量時,纔會導致該接口初始化

示例1:驗證首次使用時初始化
/**
 * 1. 對於靜態字段來講, 只有直接定義了該字段的類纔會被初始化;
 * 2. 當一個類被初始化時, 要求其父類必須初始化完成;
 * 3. -XX:+TraceClassLoading 用於追蹤類的信息並打印出來
 * 4. jvm 參數使用
 *    -XX:+<option> 表示開啓option選項
 *    -XX:-<option> 表示關閉option選項
 *    -XX:<option>=<value> 表示將option的值設置爲value
 */
public class ClassLoad_01 {

    public static void main(String[] args){
        System.out.println(Child.p_str);
    }
}

class Parent {

    public static String p_str = "Hello World!";

    /** 類被初始化時, 靜態代碼塊會被執行 **/
    static {
        System.out.println("Parent Static Block!");
    }
}

class Child extends Parent {

    public static String c_str = "Welcome!";

    /** 類被初始化時, 靜態代碼塊會被執行 **/
    static {
        System.out.println("Child Static Block!");
    }
}

輸出:

Parent Static Block!
Hello World!

注意

  • 對於靜態字段來講, 只有直接定義了該字段的類纔會被初始化;
  • 當一個類被初始化時, 要求其父類必須初始化完成;
  • -XX:+TraceClassLoading 用於追蹤類的加載信息並打印出來;

jvm 參數使用

  • -XX:+ 表示開啓option選項
  • -XX:- 表示關閉option選項
  • -XX:= 表示將option的值設置爲value
示例2:調用常量不會初始化定義常量的類
/**
 * 1. 常量在編譯階段會存入到調用這個常量的方法所在的類的常量池中
 * 2. 本質上, 調用常量時並沒有直接引用定義常量的類, 因此不會觸發定義常量的類的初始化
 */
 
/**
 * 助記符(通過javap -c反編譯查看詳細信息)
 * 1. lbc 表示將int, float, String類型的常量值從常量池中推送至棧頂
 * 2. bipush 表示將單字節(127 ~ -128)的常量值推送至棧頂
 * 3. sipush 表示將短整型(32767 ~ -32768)的常量值推送至棧頂
 * 4. iconst_1 表示將int類型的1推送至棧頂, jvm內置了int類型的-1~5的助記符(iconst_m1~iconst_5)
 */
public class ClassLoad_02 {

    public static void main(String[] args){
        System.out.println(Sub.i_2);
    }
}

class Sub {
    public static final String str = "Hello Word!";
    public static final short s = 127;
    public static final int i_1 = 128;
    public static final int i_2 = -1;
    public static final boolean b = false;

    static {
        System.out.println("Sub Static Block!");
    }
}

輸出:

Hello Word!
示例3:不能在編譯期確定值的常量,不會被放入調用者的常量池
/**
 * 1. 當一個常量的值不能在編譯器確定, 那麼該常量就不會被放到調用類的常量池中
 * 2. 在程序運行時, 會導致主動使用這個常量所在的類, 便會導致該類被初始化
 */
public class ClassLoad_03 {

    public static void main(String[] args){
        System.out.println(Child_03.str);
    }
}

class Child_03 {
    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("Child_03 Static code!");
    }
}
示例4:數組類型的對象的初始化及助記符
/**
 * 1. 當一個類被首次主動使用(7種主動使用情況的第1種)時, 會被初始化
 * 2. 不是首次主動使用的時候, 則不會再去初始化了
 * 3. 對於數組類型, 其類型是由jvm在運行期動態生成的, 表示爲[Lxxx.xxx.xxx
 *    父類型爲Object
 * 4. 對於數組, Java DOC將構成元素稱之爲Component, 實際就是將數組降低一個
 *    維度後得到的類型
 */
/**
 * 助記符:
 * 1. anewarray 表示創建一個引用類型(如類, 接口, 數組)的數組, 並將其壓入棧頂
 * 2. newarray  表示創建一個指定的基本類型(如int, float)的數組, 並將其壓入棧頂
 */
public class ClassLoad_04 {

    public static void main(String[] args){
        Child_04[] array = new Child_04[4];
        System.out.println(array.getClass());
        System.out.println("----------");
        Child_04 child_1 = new Child_04();
        System.out.println("----------");
        Child_04 child_2 = new Child_04();
    }
}

class Child_04 {

    static {
        System.out.println("Child_04 Static Code!");
    }
}
示例5:父子接口的初始化
package com.lizza;

/**
 * 1. 當一個接口初始化時, 並不要求其父接口都完成了初始化
 * 2. 只有在真正使用到父接口時(比如引用了父接口中定義的常量), 纔會完成初始化
 */
public class ClassLoad_05 {

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

interface Parent_05 {
    int a = 5;
}

interface Child_05 extends Parent_05 {
    int b = 6;
}
示例6:靜態變量在類的準備階段和初始化階段的狀態
package com.lizza;

/**
 * 1. 類的準備階段: 靜態變量會被賦予默認的初始值
 * 2. 類的初始化階段: 靜態變量會被賦予正確的初始值
 */
public class ClassLoad_06 {

    public static void main(String[] args){
        Singleton singleton = Singleton.getInstance();
    	System.out.println("count_1: " + Singleton.count_1);
    	System.out.println("count_2: " + Singleton.count_2);
    }

}

class Singleton {

    public static int count_1 = 1;

    private static Singleton singleton = new Singleton();

    private Singleton() {
        count_1 ++;
        count_2 ++;
    }

    /** 注意count_2的位置: 在類的準備階段, 靜態變量被賦予默認值; 初始化階段, 靜態變量被賦予正確的值 **/
    public static int count_2 = 0;

    public static Singleton getInstance() {
        return singleton;
    }
}

結果:

count_1: 2
count_2: 0
示例7:初始化一個類(接口)不會初始化其父接口
/**
 * 1. 初始化一個類不會初始化其父接口
 * 2. 初始化一個接口不會初始化父接口
 * 3. 只有在真正使用到父接口時(比如引用了父接口中定義的常量), 纔會完成初始化
 * 4. 當一個類被初始化的時候, 它所實現的接口是不會被初始化的; 但是接口會被加載
 * 5. 當一個類的變量爲常量時, 使用該常量, 不會導致該類被加載, 更不會被初始化
 */
public class ClassLoad_05 {

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

class Parent_05 {
    public static Thread thread = new Thread(){
        {
            System.out.println("Parent_05 inited!");
        }
    };
}

class Child_05 extends Parent_05 {
    public static int b = 6;
}

七、總結

在這裏插入圖片描述

  • 加載:將二進制的.class文件從磁盤讀入內存中
  • 驗證:確保被加載的類的正確性
  • 準備:爲類變量分配內存,設置默認值(非正確的初始值)
  • 解析:將類的常量池中類,接口,字段,方法的符號引用變爲直接引用
  • 初始化:爲變量賦予真正的初始值
  • 類實例化:爲新的對象分配內存;爲實例變量賦默認值;爲實例變量賦正確的初始值;Java編譯器爲它編譯的每一個類都至少生成一個實例初始化方法,在java的class文件中,這個實例初始化方法被稱之爲“”;針對源代碼中每一個類的構造方法,java編譯器都產生一個方法

源碼地址:https://github.com/KJGManGlory/jvm.git

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