天天看點

Java虛拟機(二):類加載子系統

類加載器子系統

一、JVM架構圖

二、 類加載子系統運作流程

加載

1.通過一個類的全限定類名擷取定義此類的二進制位元組流

2.将這個位元組流所代表的靜态存儲結構轉換為方法區運作時資料結構

3.在記憶體中生成一個代表這個類的

java.lang.Class對象

,作為方法區中這個類的各種資料的通路入口

連結:驗證、準備和解析

驗證:

1. 確定Class位元組的位元組流中包含的資訊符合JVM的要求,保證被加載類的正确性,不會危害虛拟機自身安全
 2. 驗證四種格式:檔案格式驗證、源資料驗證、位元組碼驗證和符号引用驗證
           

準備:

  1. 類變量(含有static修飾的變量)

    配置設定記憶體并且設定該類變量的初始預設值,即零值(各自類型零值均不相同)、
    1. 這裡不包含用

      final

      修飾的

      static

      ,因為

      final

      在編譯的時候就會配置設定了,準備階段會顯式初始化
    2. 這裡不會為執行個體變量配置設定初始化,類變量會配置設定在方法區中,執行個體變量是會随着對象一起配置設定到Java堆中

解析:

  1. 事實上,解析操作往往會伴随着JVM在執行完初始化之後再執行
    1. 解析就是将常量池内的符号引用轉換為直接引用的過程
    2. 符号引用

      :就是一組符号來表述所引用的目标,符号引用的字面量形式明确定義在《Java虛拟機規範》的Class檔案格式中
    3. 直接引用

      :就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄
    4. 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等
初始化:就是執行類構造器方法的clint()過程

1.

clinit()

:"class or interface initialization method",此方法不需要定義,是javac編譯器自動收集類中的所有類變量的指派動作和靜态代碼塊中的語句合并而來

2.從下圖對比中,我們即可看出clinit()是對隻有包含

static

修飾的變量或代碼塊的類初始化時才會調用

3.clinit()中指令按語句在源檔案中出現的順序執行

public class ClinitTest {

    static {
        number = 5; // 可以指派,因為static變量在類加載系統的準備階段已經完成初始值的指派
        System.out.println(number); // 但不可以調用(非法向前引用)
    }

    private static int number = 0; // 0 --> 5 --> 0

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

4.虛拟機必須保證一個類的clinit()方法在多線程下被同步加鎖即一個類隻需被clinit()一次,之後該類的内部資訊就被存儲在方法區

public class ClinitTest {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "開始");
            DemoThread thread = new DemoThread();
            System.out.println(Thread.currentThread().getName() + "結束");
        }, "線程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "開始");
            DemoThread thread = new DemoThread();
            System.out.println(Thread.currentThread().getName() + "結束");
        }, "線程2").start();
    }

}

class DemoThread{
    static {
        if (true) {
            System.out.println(Thread.currentThread().getName() + "初始化目前類");
            while (true) {

            }
        }
    }
}

線程1開始
線程2開始
線程1初始化目前類 // 線程1進入後,線程2無法重複初始化           

類加載子系統的作用

1.類加載子系統負責從磁盤或網絡中加載class檔案,class檔案在檔案開頭需要有特定的十六進制辨別:CA FE BA BE

2.加載後的Class類資訊存放于一塊成為方法區的記憶體空間。除了類資訊之外,方法區還會存放運作時常量池資訊,可能還包括字元串字面量和數字常量

3.ClassLoader隻負責class檔案的加載,至于它是否可以運作,則由Execution Engine(執行引擎)決定

4.如果調用構造器執行個體化對象,則其執行個體存放在堆區

三、類加載器分類

1.JVM支援兩種類型加載器:基于C/C++實作的引導類加載器(BootStrap ClassLoader)和基于Java實作的自定義加載器

2.從概念上來講,自定義類加載器一般指的是程式中由開發人員自定義的一類類加載器,但是Java虛拟機規範卻沒有這麼定義,而是将所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器

上圖非繼承關系,可以近似了解為包含關系

3.在程式中我們最常見的類加載器是:引導類加載器(BootStrap ClassLoader)、自定義類加載器:擴充類加載器(Extension ClassLoader)、系統(應用)類加載器(System(App) ClassLoader)和使用者自定義類加載器(User-Defined ClassLoader))

加載器具體介紹

引導類加載器(BootStrap ClassLoader)

1.這個類加載使用C/C++語言實作的,嵌套在JVM内部

2.它用來加載java的核心庫(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路徑下的内容),用于提供JVM自身需要的類

3.并不繼承自java.lang.ClassLoader,沒有父加載器

4.加載拓展類和應用程式類加載器,并指定為他們的父加載器,即ClassLoader

5.出于安全考慮,BootStrap啟動類加載器隻加載包名為java、javax、sun等開頭的類           

拓展類加載器(Extension ClassLoader)

1.java語言編寫 ,由sun.misc.Launcher$ExtClassLoader實作。

2.派生于ClassLoader類

3.從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴充目錄)下加載類庫。如果使用者建立的JAR放在此目錄下,也會由拓展類加載器自動加載           

應用程式類加載器(系統類加載器,AppClassLoader)

1.java語言編寫, 由sun.misc.Launcher$AppClassLoader實作。

2.派生于ClassLoader類

3.它負責加載環境變量classpath或系統屬性 java.class.path指定路徑下的類庫

4.該類加載器是程式中預設的類加載器,一般來說,java應用的類都是由它來完成加載

5.通過ClassLoader#getSystemClassLoader()方法可以擷取到該類加載器           

4.擷取類的加載器

/**
 * ClassLoader加載
 */
public class ClassLoaderTest {
    public static void main(String[] args) {
        //擷取系統類加載器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //擷取其上層  擴充類加載器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6

        //擷取其上層 擷取不到引導類加載器
        ClassLoader bootStrapClassLoader = extClassLoader.getParent();
        System.out.println(bootStrapClassLoader);//null

        //對于使用者自定義類來說:使用系統類加載器進行加載
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String 類使用引導類加載器進行加載的  -->java核心類庫都是使用引導類加載器加載的
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null擷取不到間接證明了String 類使用引導類加載器進行加載的

    }
}           

四、雙親委派機制

Java虛拟機對Class檔案采用的是按需加載的方式,也就是說當需要使用該類時才會将它的Class檔案加載到記憶體生成的class對象。而且加載某個類的class檔案時,Java虛拟機采用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式

雙親委派機制原理
雙親委派機制優勢

1.避免類的重複加載

2.保護程式安全,防止核心API被随意修改

我們定義包名的時候起名java.lang類名為String,這時如果沒有雙親委派機制,我們就會将String這種資料類型變成我們自己寫的類型           

3.保證核心API包的通路權限

五、JVM中表示兩個Class對象是否為同一個對象

1.在JVM中表示兩個class對象是否為同一個類存在的兩個必要條件

​ ①.類的完整類名必須一緻,包括包名

​ ②.即使類的完整類名一緻,同時要求加載這個類的ClassLoader(指ClassLoader執行個體對象)必須相同:是引導類加載器、還是定義類加載器

2.換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class檔案,被同一個虛拟機所加載,但隻要加載它們的ClassLoader執行個體對象不同,那麼這兩個類對象也是不相等的.

3.對類加載器的引用,JVM必須知道一個類型是有啟動類加載器加載的還是由使用者類加載器加載的。如果一個類型由使用者類加載器加載的,那麼JVM會将這個類加載器的一個引用作為類型資訊的一部分儲存在方法區中。當解析一個類型到另一個類型的引用的時候,JVM需要保證兩個類型的加載器是相同的

六、類的主動使用和被動使用

Java程式對類的使用方式分為:主動使用和被動使用,即是否調用了clinit()方法

下面時類的主動使用,其它使用都被看做類的被動使用,不會産生類的初始化

1.建立類的執行個體

2.通路某各類或接口的靜态變量,或者對靜态變量指派

3.調用類的靜态方法

4.反射:比如Class.forName(com.dsh.jvm.xxx)

5.初始化一個類的子類

6.Java虛拟機啟動時被标明為啟動類的類

7.JDK7開始提供的動态語言支援:java.lang.invoke.MethodHandle執行個體的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化