JVM 類加載機制和字節碼技術

1. 簡述

典型的Java程序執行流程如下:

  1. 我們在本地編寫完Java源程序;
  2. IDE自動幫我們編譯成.class文件(也可以手動通過javac命令編譯),然後打包成jar包或者war包;
  3. 接着,執行java -jar命令或直接部署到web容器中來運行程序;
  4. 運行時,OS會啓動一個JVM進程,JVM會採用類加載器將各種.class文件中包含的Java類加載到內存中;
  5. 最後,JVM基於自己的字節碼執行引擎,來執行加載到內存中的那些類。

2. 類加載機制

2.1 類加載器

類加載器可以大致劃分爲以下三類:

  • Bootstrap ClassLoader: 主要負責加載 JDK 安裝目錄下的核心類庫(比如/lib目錄下的類),這些核心類庫是JVM運行時自身需要用到的。開發者不能直接在Java程序中使用
  • Extension ClassLoader:主要負責加載 JDK 安裝目錄下的擴展類庫(比如/lib/ext目錄下的類),這些擴展類庫是JDK按照功能進行模塊劃分的,一般也是Java程序運行所必需的。比如使用maven引入的第三方包
  • Application ClassLoader:負責加載用戶類路徑(classpath)所指定的類,可以簡單的理解成負責加載用戶自己開發的Java類。
  • 除了上述提供到三種類加載器外,開發者也可以自定義類加載器,根據自己的需求去加載類。

2.2 雙親委派機制

JVM的類加載器是有親子層級結構的,層級結構如下圖:

 

當我們的類加載器需要加載一個類時,首先會委派給自己的父類加載器去加載,最終傳到到頂層 的類加載器去加載;如果某個父類加載器發現在自己負責的範圍內並沒有找到這個類,就會下推加載權力給自己的子類加載器。

以上圖爲例:

  1. 當Application ClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器Extension ClassLoader去完成;
  2. 當Extension ClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給Bootstrap ClassLoader去完成;
  3. 如果Bootstrap ClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用Extension ClassLoader來嘗試加載;
  4. Extension ClassLoader也加載失敗,則會使用Application ClassLoader來加載,如果Application ClassLoader也加載失敗,則會報出ClassNotFoundException異常。

優點

雙親委派機制的優點很明顯,可以避免類的重複加載,當父親已經加載了該類時,子ClassLoader就沒有必要再加載一次。

 

2.3 完整流程

類從.class二進制數據被加載到 JVM 內存中開始,到卸載出內存爲止,它的整個生命週期包括:

加載(Loading)驗證(Verification)準備(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸載(Unloading),共7個階段。

 

  • 加載階段: 很簡單,當程序執行到需要的類時,JVM就會通過類加載器 將其加載到內存中.
  • 驗證階段:根據Java虛擬機規範,需要對加載進來的“.class”文件的內容進行校驗,包括驗證文件格式、元數據、字節碼、符號引用等各種信息,以確認是否符合指定的規範
  • 準備階段:主要是爲類及其靜態字段分配內存,並將其初始化爲默認值
  • 解析階段:實際上是把類的符號引用替換爲直接引用的過程
  • 初始化階段:之前說過,JVM會在準備階段給類的靜態字段分配空間和默認值。而在初始化階段,就會正式執行類的初始化代碼,對類進行初始化操作
public class ReplicaManager {
    public static int flushInterval = Configuration.getInt("replica.flush.interval");
    public static Map<String,Replica> replicas;

    static {
        loadReplicaFromDish():
    }

    public static void loadReplicaFromDish(){
        this.replicas = new HashMap<String,Replica>();
    }
}

對於flushInternal變量,我們通過一個getInt方法從配置中獲取值並進行賦值,這個賦值動作在準備階段是不會執行的,而是在初始化階段執行。另外,對於static靜態代碼塊,也是在這個階段執行的

在初始化階段,如果JVM初始化某個類時,發現其父類還沒有初始化完成的話,會首先去加載其父類,加載策略就是上一節提到的雙親委派機制。

  • 卸載階段:就是當對象不再需要使用時,JVM需要進行垃圾回收

3. tomcat類加載機制

Tomcat的類加載體系如下圖,藍色部分是Tomcat繼承Application ClassLoader實現的自定義類加載器:

 Common、Catalina、Shared類加載器用來加載Tomcat自身的一些核心基礎類庫。同時,Tomcat爲每一個部署在其內的web應用都分配了一個對應的WebApp類加載器,就是這個類加載器負責加載我們部署的這個web應用的類,每一個WebApp只負責加載自己對應的那個web應用的class文件,不會傳導給上層類加載器去加載。所以,Tomcat的類加載器設計其實是打破了雙親委派機制的

 4. 字節碼技術

我們知道,我們編寫的 Java 代碼都是要被編譯成字節碼後才能放到 JVM 裏執行的,而字節碼一旦被加載到虛擬機中,就可以被解釋執行。字節碼文件(.class)就是普通的二進制文件,它是通過 Java 編譯器生成的。而只要是文件就可以被改變,如果我們用特定的規則解析了原有的字節碼文件,對它進行修改或者乾脆重新定義,就可以改變代碼行爲了。

可在程序中動態生成.class文件,還可以動態修改.class文件

ASM

ASM 是它們中最強大的一個,使用它可以動態修改類、方法,甚至可以重新定義類,連 CGLib動態代理 底層都是用 ASM 實現的。但是它使用起來挺難的,所以我們使用javassist來實現。

javassist

ASM使用起來有點麻煩,所以我們使用javassist來做,雖然性能較ASM略低,但是它使用起來簡單

示例

使用javassist動態創建一個User類

//使用java字節碼技術創建字節碼
public class Test002 {

	public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException {
		ClassPool pool = ClassPool.getDefault();
		// 1.創建user類
		CtClass userClass = pool.makeClass("com.itmayiedu.entity.User");
		// 2.創建name 和age屬性
		CtField nameField = CtField.make("	private String name;", userClass);
		CtField ageField = CtField.make("	private Integer age;", userClass);
		// 3.添加屬性
		userClass.addField(nameField);
		userClass.addField(ageField);
		// 4.創建方法
		CtMethod nameMethod = CtMethod.make("public String getName() {return name;}", userClass);
		// 5.添加方法
		userClass.addMethod(nameMethod);
		// 6.添加構造函數
		CtConstructor ctConstructor = new CtConstructor(
				new CtClass[] { pool.get("java.lang.String"), pool.get("java.lang.Integer") }, userClass);

		ctConstructor.setBody("	{ this.name = name; this.age = age; }");
		userClass.addConstructor(ctConstructor);

		// 生成class文件
		userClass.writeFile("F:/test");
	}

}

 反編譯JD-GUI

編譯後的.class文件可以通過JD-GUI工具來反編譯查看是否正確

參考博客:https://www.tpvlog.com/article/85#menu_1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章