1. JVM核心類載入器及類載入的全過程

運行環境:

下面說明一下我的運行環境。我是在mac上操作的. 先找到mac的java地址. 從~/.bash_profile中可以看到

java的home目錄是: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home


一. 類載入的過程

1.1 類載入器初始化的過程

假如現在有一個java類 com.lxl.jvm.Math類, 裡面有一個main方法

package com.lxl.jvm;

public class Math {
    public static int initData = 666;
    public static User user = new User();

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }
}

這個方法很簡單, 通常我們直接執行main方法就ok, 可以運行程式了, 那麼點擊運行main方法, 整個過程是如何被載入運行的呢? 為什麼點擊執行main, 就能得到結果呢?

先來看看答題的類載入流程(宏觀流程), 如下圖:

備註:

1. windows上的java啟動程式是java.exe, mac下是java

2. c語言部分,我們做了解, java部門是需要掌握的部分.

第一步: java調用底層的jvm.dll文件創建java虛擬機(這一步由C++實現) . 這裡java.exe是c++寫的程式碼, 調用的jvm.dll也是c++底層的一個函數. 通過調用jvm.dll文件(dll文件相當於java的jar包), 會創建java虛擬機. java虛擬機的啟動都是c++程式實現的.

第二步:在啟動虛擬機的過程中, 會創建一個引導類載入器的實例. 這個引導類的載入器是C語言實現的. 然後jvm虛擬機就啟動起來了.

第三步: 接下來,C++語言會調用java的啟動程式.剛剛只是創建了java虛擬機, java虛擬機裡面還有很多啟動程式. 其中有一個程式叫做Launcher. 類全稱是sun.misc.Launcher. 通過啟動這個java類, 會由這個類引導載入器載入並創建很多其他的類載入器. 而這些載入器才是真正啟動並載入磁碟上的位元組碼文件.

第四步:真正的去載入本地磁碟的位元組碼文件,然後啟動執行main方法.(這一步後面會詳細說,到底是怎麼載入本地磁碟的位元組碼文件的。)

第五步:main方法執行完畢, 引導類載入器會發起一個c++調用, 銷毀JVM

以上就是啟動一個main方法, 這個類載入的全部過程

下面, 我們重點來看一下, 我們的類com.lxl.Math是怎麼被載入到java虛擬機裡面去的?  

1.2 類載入的過程

上面的com.lxl.jvm.Math類最終會生成clas位元組碼文件. 位元組碼文件是怎麼被載入器載入到JVM虛擬機的呢?

類載入有五步:載入, 驗證, 準備, 解析, 初始化. 那麼這五步都是幹什麼的呢?我們來看一下

我們的類在哪裡呢? 在磁碟里(比如: target文件夾下的class文件), 我們先要將class類載入到記憶體中. 載入到記憶體區域以後, 不是簡簡單單的轉換成二進位位元組碼文件,他會經過一系列的過程. 比如: 驗證, 準備, 解析, 初始化等. 把這一些列的資訊轉變成內元資訊, 放到記憶體裡面去. 我們來看看具體的過程

第一步: 載入.

將class類載入到java虛擬機的記憶體里去, 在載入到記憶體之前, 會有一系列的操作。第一步是驗證位元組碼。

第二步:驗證

驗證位元組碼載入是否正確, 比如:打開一個位元組碼文件。打眼一看, 感覺像是亂碼, 實際上不是的. 其實,這裡面每個字元串都有對應的含義. 那麼文件裡面的內容我們能不能替換呢?當然不能, 一旦替換, 就不能執行成功了. 所以, 第一步:驗證, 驗證什麼呢?

驗證位元組碼載入是否正確: 格式是否正確. 內容是否符合java虛擬機的規範.

第三步:準備

驗證完了, 接下來是準備. 準備幹什麼呢? 比如我們的類Math, 他首先會給Math里的靜態變數賦值一個初始值. 比如我們Math里有兩個靜態變數

public static int initData = 666;
public static User user = new User();

在準備的過程中, 就會給這兩個變數賦初始值, 這個初始值並不是真實的值, 比如initData的初始值是0. 如果是boolean類型, 就賦值為false. 也就是說, 準備階段賦的值是jvm固定的, 不是我們定義的值.如果一個final的常量, 比如public static final int name=”zhangsan”, 那麼他在初始化的時候, 是直接賦初始值”zhangsan”的. 這裡只是給靜態變數賦初始值

第四步:解析

接下來說說解析的過程. 解析的過程略微複雜, 解析是將”符號引用”轉變為直接引用.

什麼是符號引用呢?

比如我們的程式中的main方法. 寫法是固定的, 我們就可以將main當成一個符號. 比如上面的initData, int, static, 我們都可以將其稱之為符號, java虛擬機內部有個專業名詞,把他叫做符號. 這些符號被載入到記憶體里都會對應一個地址. 將”符號引用”轉變為直接引用, 指的就是, 將main, initData, int等這些符號轉變為對應的記憶體地址. 這個地址就是程式碼的直接引用. 根據直接引用的值,我們就可以知道程式碼在什麼位置.然後拿到程式碼去真正的運行.

將符號引用轉變為”記憶體地址”, 這種有一個專業名詞, 叫靜態鏈接. 上面的解析過程就相當於靜態鏈接的過程. 類載入期間,完成了符號到記憶體地址的轉換. 有靜態鏈接, 那麼與之對應的還有動態鏈接.

什麼是動態鏈接呢?

public static void main(String[] args) {
        Math math = new Math();
        math.compute();
    }

比如:上面這段程式碼, 只有當我運行到math.compute()這句話的時候, 才回去載入compute()這個方法. 也就是說, 在載入的時候, 我不一定會把compute()這個方法解析成記憶體地址. 只有當運行到這行代買的時候, 才會解析.

我們來看看彙編程式碼

javap -v Math.class
Classfile /Users/luoxiaoli/Downloads/workspace/project-all/target/classes/com/lxl/jvm/Math.class
  Last modified 2020-6-27; size 777 bytes
  MD5 checksum a6834302dc2bf4e93011df4c0b774158
  Compiled from "Math.java"
public class com.lxl.jvm.Math
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#35         // java/lang/Object."<init>":()V
   #2 = Class              #36            // com/lxl/jvm/Math
   #3 = Methodref          #2.#35         // com/lxl/jvm/Math."<init>":()V
   #4 = Methodref          #2.#37         // com/lxl/jvm/Math.compute:()I
   #5 = Fieldref           #2.#38         // com/lxl/jvm/Math.initData:I
   #6 = Class              #39            // com/lxl/jvm/User
   #7 = Methodref          #6.#35         // com/lxl/jvm/User."<init>":()V
   #8 = Fieldref           #2.#40         // com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;
   #9 = Class              #41            // java/lang/Object
  #10 = Utf8               initData
  #11 = Utf8               I
  #12 = Utf8               user
  #13 = Utf8               Lcom/lxl/jvm/User;
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               Lcom/lxl/jvm/Math;
  #21 = Utf8               compute
  #22 = Utf8               ()I
  #23 = Utf8               a
  #24 = Utf8               b
  #25 = Utf8               c
  #26 = Utf8               main
  #27 = Utf8               ([Ljava/lang/String;)V
  #28 = Utf8               args
  #29 = Utf8               [Ljava/lang/String;
  #30 = Utf8               math
  #31 = Utf8               MethodParameters
  #32 = Utf8               <clinit>
  #33 = Utf8               SourceFile
  #34 = Utf8               Math.java
  #35 = NameAndType        #14:#15        // "<init>":()V
  #36 = Utf8               com/lxl/jvm/Math
  #37 = NameAndType        #21:#22        // compute:()I
  #38 = NameAndType        #10:#11        // initData:I
  #39 = Utf8               com/lxl/jvm/User
  #40 = NameAndType        #12:#13        // user:Lcom/lxl/jvm/User;
  #41 = Utf8               java/lang/Object
{
  public static int initData;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public static com.lxl.jvm.User user;
    descriptor: Lcom/lxl/jvm/User;
    flags: ACC_PUBLIC, ACC_STATIC

  public com.lxl.jvm.Math();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lxl/jvm/Math;

  public int compute();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: bipush        10
         9: imul
        10: istore_3
        11: iload_3
        12: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 2
        line 10: 4
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lcom/lxl/jvm/Math;
            2      11     1     a   I
            4       9     2     b   I
           11       2     3     c   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/lxl/jvm/Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
            8       6     1  math   Lcom/lxl/jvm/Math;
    MethodParameters:
      Name                           Flags
      args

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: sipush        666
         3: putstatic     #5                  // Field initData:I
         6: new           #6                  // class com/lxl/jvm/User
         9: dup
        10: invokespecial #7                  // Method com/lxl/jvm/User."<init>":()V
        13: putstatic     #8                  // Field user:Lcom/lxl/jvm/User;
        16: return
      LineNumberTable:
        line 4: 0
        line 5: 6
}
SourceFile: "Math.java"

使用這個指令, 就可以查看Math的二進位文件. 其實這個文件,就是上面那個二進位程式碼文件.

看看這裡面有什麼東西?

類的名稱, 大小,修改時間, 大版本,小版本, 訪問修飾符等等

 Last modified 2020-6-27; size 777 bytes
  MD5 checksum a6834302dc2bf4e93011df4c0b774158
  Compiled from "Math.java"
public class com.lxl.jvm.Math
  minor version: 0
  major version: 52

還有一個Constant pool 常量池. 這個常量池裡面有很多東西. 我們重點看中間哪一行. 第一列表示一個常量的標誌符, 這個標識符可能在其他地方會用到. 第二列就表示常量內容.

Constant pool:
   #1 = Methodref          #9.#35         // java/lang/Object."<init>":()V
   #2 = Class              #36            // com/lxl/jvm/Math
   #3 = Methodref          #2.#35         // com/lxl/jvm/Math."<init>":()V
   #4 = Methodref          #2.#37         // com/lxl/jvm/Math.compute:()I
   #5 = Fieldref           #2.#38         // com/lxl/jvm/Math.initData:I
   #6 = Class              #39            // com/lxl/jvm/User
   #7 = Methodref          #6.#35         // com/lxl/jvm/User."<init>":()V
   #8 = Fieldref           #2.#40         // com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;
   #9 = Class              #41            // java/lang/Object
  #10 = Utf8               initData
  #11 = Utf8               I
  #12 = Utf8               user
  #13 = Utf8               Lcom/lxl/jvm/User;
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               Lcom/lxl/jvm/Math;
  #21 = Utf8               compute

這些標識符在後面都會被用到, 比如main方法

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/lxl/jvm/Math
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method compute:()I
        12: pop
        13: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
            8       6     1  math   Lcom/lxl/jvm/Math;
    MethodParameters:
      Name                           Flags
      args

這裡面就用到了#2 #3 #4 ,這都是標識符的引用.

第一句: new了一個Math(). 我們看看彙編怎麼寫的?

         0: new           #2                  // class com/lxl/jvm/Math

new + #2. #2是什麼呢? 去常量池裡看, #2代表的就是Math類

   #2 = Class              #36            // com/lxl/jvm/Math

這裡要說的還是math.compute()這個方法, 不是在類載入的時候就被載入到記憶體中去了, 而是運行main方法的時候, 執行到這行程式碼才被載入進去, 這個過程叫做動態鏈接.

類載入的時候, 我們可以把”解析”理解為靜態載入的過程. 一般像靜態方法(例如main方法), 獲取其他不變的靜態方法會被直接載入到記憶體中, 因為考慮到性能, 他們載入完以後就不會變了, 就直接將其轉變為在記憶體中的程式碼位置.

而像math.compute()方法, 在載入過程中可能會變的方法(比如compute是個多態,有多個實現), 那麼在初始化載入的時候, 我們不會到他會調用誰, 只有到運行時才能知道程式碼的實現, 所以在運行的時候在動態的去查詢他在記憶體中的位置, 這個過程就是動態載入

第五步: 初始化

對類的靜態變數初始化為指定的值. 執行靜態程式碼塊. 比如程式碼

public static int initData = 666;

在準備階段將其賦值為0, 而在初始化階段, 會將其賦值為設定的666  

1.3 類的懶載入

類被載入到方法區中以後,主要包含:運行時常量池, 類型資訊, 欄位資訊, 方法資訊, 類載入器的引用, 對應class實例的引用等資訊.

什麼意思呢? 就是說, 當一個類被載入到記憶體, 這個類的常量,有常量名, 類型, 域資訊等; 方法有方法名, 返回值類型, 參數類型, 方法作用域等符號資訊都會被載入放入不同的區域.

注意: 如果主類在運行中用到其他類,會逐步載入這些類, 也就是說懶載入. 用到的時候才載入.

package com.lxl.jvm;
public class TestDynamicLoad {
    static {
        System.out.println("********Dynamic load class**************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("*********load test*****************");
        B b = null; // 這裡的b不會被載入, 除非new B();
    }
}

class A {
    static {
        System.out.println("********load A**************");
    }

    public A(){
        System.out.println("********initial A**************");
    }
}

class B {
    static {
        System.out.println("********load B**************");
    }

    public B(){
        System.out.println("********initial B**************");
    }
}

這裡定義了兩個類A和B, 當使用到哪一個的時候, 那個類才會被載入, 比如:main方法中, B沒有被用到, 所以, 他不會被載入到記憶體中.

運行結果

********Dynamic load class**************
********load A**************
********initial A**************
*********load test*****************

我們看到A類被載入了,而B類沒有被載入,原因是B類只聲明了,沒有用到。

總結幾點如下:

  1. 靜態程式碼塊在構造方法之前執行

  2. 沒有被真正使用的類不會被載入

二. 類載入器

2.1 類載入器的類型

類主要通過類載入器來載入, java裡面有如下幾種類載入器

1. 引導類載入器(Bootstrap ClassLoader)

在上面類載入流程中,說到在 [啟動虛擬機的過程中, 會創建一個引導類載入器的實例] 這個引導類載入器的目的是什麼呢?載入類

引導類載入器主要負責載入最最核心的java類型。 這些類庫位於jre目錄的lib目錄下**. 比如:rt.jar, charset.jar等,

2. 擴展類載入器(Ext ClassLoader)

擴展類載入器主要是用來載入擴展的jar包。 載入jar的目錄位於jre目錄的lib/ext擴展目錄中的jar包

3. 應用程式類載入器(App CloassLoader)

主要是用來載入用戶自己寫的類的。 負責載入classPath路徑下的類包

4. 自定義類載入器

負責載入用戶自定義路徑下的類包

引導類載入器是由C++幫我們實現的, 然後c++語言會通過一個Launcher類將擴展類載入器(ExtClassLoader)和應用程式類載入器(AppClassLoader)構造出來, 並且把他們之間的關係構建好.

2.2 案例

案例一:測試jdk自帶的類載入器

package com.lxl.jvm;
import sun.misc.Launcher;
import java.net.URL;
public class TestJDKClassLoader {
    public static void main(String[] args) {
        /**
         * 第一個: String 是jdk自身自帶的類,位於jre/lib核心目錄下, 所以, 他的類載入器是引導類載入器
         * 第二個: 加密類的classloader, 這是jdk擴展包的一個類
         * 第三個: 是我們當前自己定義的類, 會被應用類載入器載入
         */
        System.out.println(String.class.getClassLoader()); 						         System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
    }
}

我們來看這個簡單的程式碼, 運行結果:

null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader

解析:
 第一個: String 是jdk自身自帶的類, 所以, 他的類載入器是引導類載入器,引導類載入器是c++程式碼,所以這裡返回null
 第二個: 加密類的classloader, 這是jdk擴展包的一個類, jdk擴展包裡面使用的是extClassLoader類載入器載入的
 第三個: 是我們當前自己定義的類, 會被AppClassLoader應用程式載入器載入.

我們看到ExtClassLoader和AppClassLoader都是Launcher類的一部分. 那Launcher類是什麼東西呢?

上面有提到, Launcher類是jvm啟動的時候由C++調用啟動的一個類. 這個類引導載入器載入並創建其他的類載入器。

那麼,第一個bootstrap引導類載入器, 那引導類載入器返回的為什麼是null呢?

因為bootstrap引導類載入器, 他不是java的對象, 他是c++生成的對象, 所以這裡是看不到的

案例二: BootstrapClassLoad和ExtClassLoader、AppClassLoader的關係

如上圖,左邊是C語言程式程式碼實現, 右邊是java程式碼實現。這裡是跨語言調用,JNI實現了有c++向java跨語言調用。c語言調用的第一個java類是Launcher類。

從這個圖中我們可以看出,C++調用java創建JVM啟動器, 其中一個啟動器是Launcher, 他實際是調用了sun.misc.Launcher類的getLauncher()方法. 那我們就從這個方法入手看看到底是如何運行的?

我們看到Lanucher.java類是在核心的rt.jar包里的,Lanucher是非常核心的一個類。

我們看到getLauncher()類直接返回了launcher. 而launcher是一個靜態對象變數, 這是一個單例模式

C++調用了getLauncher()–>直接返回了lanucher對象, 而launcher對象是在構建類的時候就已經初始化好了. 那麼,初始化的時候做了哪些操作呢?接下來看看他的構造方法.

在構造方法里, 首先定義了一個ExtClassLoader. 這是一個擴展類載入器, 擴展類載入器調用的是getExtClassLoader(). 接下來看一看getExtClassLoader這個方法做了什麼?

這是一個典型的多執行緒同步的寫法。

在這裡, 判斷當前對象是否初始化過, 如果沒有, 那麼就創建一個ExtClassLoader()對象, 看看createExtClassLoader()這個方法做了什麼事呢?

doPrivileged是一個許可權校驗的操作, 我們可以先不用管, 直接看最後一句, return new Launcher.ExtClassLoader(var1). 直接new了一個ExtClassLoader, 其中參數是var1, 代表的是ext擴展目錄下的文件.

在ExtClassLoader(File[] var1)這個方法中, 這裡第一步就是調用了父類的super構造方法. 而ExtClassLoader繼承了誰呢? 我們可以看到他繼承了URLClassLoader.

而URLClassLoader是幹什麼用的呢? 其實聯想一下大概能夠猜數來, 這裡有一些文件路徑, 通過文件路徑載入class類.

我們繼續看調用的super(parent), 我們繼續往下走, 就會看到調用了ClassLoader介面的構造方法:

這裡設置了ExtClassLoader的parent是誰? 注意看,我們發現, ExtClassLoader的parent類是null.

這就是傳遞過來的parent類載入器, 那麼這裡的parent類載入器為什麼是null呢? 因為, ExtClassLoader的父類載入器是誰呢? 他是Bootstrap ClassLoader. 而BootStrap ClassLoader是C++的類載入器, 我們不能直接調用它, 所以, 設置為null.

其實, ExtClassLoader在初始化階段就是調用了ExtClassLoader方法, 初始化了ExtClassLoader類

接下來,我們回到Launcher的構造方法, 看看Launcher接下來又做了什麼?

可以看到, 接下來調了AppClassLoader的getAppClassLoader(var1), 這個方法. 需要注意一下的是var1這個參數. var1是誰呢? 向上看, 可以看到var1是ExtClassLoader.

這是AppClassLoader, 應用程式類載入器, 這個類是載入我們自己定義的類的類載入器. 他也是繼承自URLClassLoader.

我們來看看getAppClassLoader(final ClassLoader var0)方法. 這個方法的參數就是上面傳遞過來的ExtClassLoader

這裡第一句話就是獲取當前項目的class 文件路徑, 然後將其轉換為URL. 並調用了Launcher.AppClassLoader(var1x, var0), 其中var1x是class類所在的路徑集合, var0是擴展的類載入器ExtClassLoader, 接下來, 我們進入到這個方法里看一看

AppClassLoader直接調用了其父類的構造方法, 參數是class類路徑集合, 和ExtClassLoader


最後, 我們看到, 將ExtClassLoader傳遞給了parent變數. 這是定義在ClassLoader中的屬性, 而ClassLoader類是所有類載入器的父類. 因此, 我們也可以看到AppClassLoader的父類載入器是ExtClassLoader

同時, 我們也看到了, C++在啟動JVM的時候, 調用了Launcher啟動類, 這個啟動類同時載入了ExtClassLoader和AppClassLoader.

public static void main(String[] args) {
        
  ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
  ClassLoader extClassLoader = appClassLoader.getParent();
  ClassLoader bootstrapClassLoad = extClassLoader.getParent();


  System.out.println("bootstrap class loader: " + bootstrapClassLoad);
  System.out.println("ext class loader " + extClassLoader);
  System.out.println("app class loader "+ appClassLoader);
}

通過這個demo, 我們也可以看出, appClassLoader的父類是extClassLoader, extClassLoader的父類是bootstrapClassLoader

輸出結果:

bootstrap class loader: null
ext class loader sun.misc.Launcher$ExtClassLoader@2a84aee7
app class loader sun.misc.Launcher$AppClassLoader@18b4aac2