執行環境:

下面說明一下我的執行環境。我是在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