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 

通過上面的源碼分析,我們發現引導類加載器創建並加載了擴展類加載器和應用類加載器。而擴展類加載器的父加載器是引導類加載器。應用類加載器的父加載器是擴展類加載器。這個結構,決定了后面類的加載方式,也就是雙親委派機制。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM