首頁  >  文章  >  Java  >  Java的類別載入機制

Java的類別載入機制

(*-*)浩
(*-*)浩轉載
2019-08-27 16:21:192614瀏覽

很長一段時間裡,我對 Java 的類別載入機制都非常的抗拒,因為我覺得太難理解了。但為了成為優秀的 Java 工程師,我決定硬著頭皮研究一下。

Java的類別載入機制

01、字節碼

#在聊Java 類別載入機制之前,需要先了解Java 字節碼,因為它和類別載入機制息息相關。

電腦只認識 0 和 1,所以任何語言寫的程式都需要編譯成機器碼才能被電腦理解,然後執行,Java 也不例外。

Java 在誕生的時候喊出了一個非常牛逼的口號:“Write Once, Run Anywhere”,為了達成這個目的,Sun 公司發布了許多可以在不同平台(Windows、Linux)上運行的Java 虛擬機器(JVM)-負責載入和執行Java 編譯後的字節碼。

 Java的類別載入機制

到底 Java 位元組碼是什麼樣子,我們借助一段簡單的程式碼來看一看。

原始碼如下:

package com.cmower.java_demo;

public class Test {

    public static void main(String[] args) {
        System.out.println("版权声明");
    }

}

程式碼編譯通過後,透過 xxd Test.class 指令查看這個字節碼檔案。

xxd Test.class
00000000: cafe babe 0000 0034 0022 0700 0201 0019  .......4."......
00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f  com/cmower/java_
00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a  demo/Test......j
00000030: 6176 612f 6c61 6e67 2f4f 626a 6563 7401  ava/lang/Object.
00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100  ..<init>...()V..
00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601  .Code...........
00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c  ..LineNumberTabl

感覺有點懵逼,對不對?

懵就對了。

這段字節碼中的 cafe babe 被稱為“魔數”,是 JVM 識別 .class 檔案的標誌。文件格式的自訂者可以自由選擇魔數值(只要沒用過),比如說 .png 檔案的魔數是 8950 4e47。

至於其他內容嘛,可以選擇忘記了。

02、類別載入過程

了解了 Java 字節碼後,我們來聊聊 Java 的類別載入過程。

Java 的類別載入程序可以分為 5 個階段:載入、驗證、準備、解析和初始化。這 5 個階段一般是順序發生的,但在動態綁定的情況下,解析階段發生在初始化階段之後。

1)Loading(載入)

JVM 在該階段的主要目的是將字節碼從不同的資料來源(可能是class 檔案、也可能是jar 包,甚至網絡)轉換為二進位位元組流載入到記憶體中,並產生一個代表該類別的java.lang.Class 物件。

2)Verification(驗證)

JVM 會在該階段對二進位位元組流進行校驗,只有符合 JVM 字節碼規範的才能被 JVM 正確執行。此階段是確保 JVM 安全的重要屏障,以下是一些主要的檢查。

確保二進位位元組流格式符合預期(例如說是否以 cafe bene 開頭)。

是否所有方法都遵守存取控製關鍵字的限定。

方法呼叫的參數個數和型別是否正確。

確保變數在使用前被正確初始化了。

檢查變數是否被賦予恰當類型的值。

3)Preparation(準備)

JVM 會在該階段對類別變數(也稱為靜態變量,static 關鍵字修飾的)分配記憶體並初始化(對應資料類型的預設初始值,如0、0L、null、false 等)。

也就是說,假如有這樣一段程式碼:

public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二";

chenmo 不會被分配內存,而 wanger 會;但 wanger 的初始值不是「王二」而是 null。

要注意的是,static final 修飾的變數被稱為常數,和類別變數不同。常數一旦賦值就不會改變了,所以 cmower 在準備階段的值為「沉默王二」而不是 null。

4)Resolution(解析)

此階段將常數池中的符號參考轉換為直接引用。

what?符號引用,直接引用?

符號引用以一組符號(任何形式的字面量,只要在使用時能夠無歧義的定位到目標即可)來描述所引用的目標。

在編譯時,Java 類別並不知道所引用的類別的實際位址,因此只能使用符號參考來取代。例如 com.Wanger 類別引用了 com.Chenmo 類,編譯時 Wanger 類別並不知道 Chenmo 類別的實際記憶體位址,因此只能使用符號 com.Chenmo。

直接引用透過符號引用進行解析,找到引用的實際記憶體位址。

5)Initialization(初始化)

該階段是類別載入過程的最後一步。在準備階段,類別變數已經被賦過預設初始值,而在初始化階段,類別變數將被賦值為程式碼期望賦的值。換句話說,初始化階段是執行類別構造器方法的過程。

oh,no,上面這段話說得很抽象,不好理解,對不對,我來舉個例子。

String cmower = new String("沉默王二");

上面這段程式碼使用了new 關鍵字來實例化一個字串對象,那麼這時候,就會呼叫String類別的建構方法對cmower 進行實例化。

03、類別載入器

聊完類別載入過程,就得聊聊類別載入器。

一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试

ClassNotFoundException 和 NoClassDefFoundError 等异常。

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。

站在程序员的角度来看,Java 类加载器可以分为三种。

1)启动类加载器(Bootstrap Class-Loader),加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar。

2)扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext 包下面的 jar 文件。

3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。

来来来,通过一段简单的代码了解下。

public class Test {

	public static void main(String[] args) {
		ClassLoader loader = Test.class.getClassLoader();
		while (loader != null) {
			System.out.println(loader.toString());
			loader = loader.getParent();
		}
	}

}

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;然后通过 loader.getParent() 可以获取类加载器的上层类加载器。

这段代码的输出结果如下:

sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742

第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader 类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader 类的实例。那启动类加载器呢?

按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent() 返回 null。所以没有输出。

04、双亲委派模型

如果以上三种类加载器不能满足要求的话,程序员还可以自定义类加载器(继承 java.lang.ClassLoader 类),它们之间的层级关系如下图所示。

Java的類別載入機制

这种层次关系被称作为双亲委派模型:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类。

PS:双亲委派模型突然让我联想到朱元璋同志,这个同志当上了皇帝之后连宰相都不要了,所有的事情都亲力亲为,只有自己没精力没时间做的事才交给大臣们去干。

使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。

上文中曾提到,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。

以上是Java的類別載入機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除