JVM從入門到實戰:從字節碼組成、類生命周期到雙親委派及打破雙親委派機制

摘要:本文圍繞 Java 字節碼與類加載機制展開,詳解字節碼文件組成、類的生命周期介紹類加載器分類雙親委派機制及打破該機制的方式,還闡述了線程上下文類加載器與 SPI 機制在 JDBC 驅動加載中的應用,幫助深入理解 Java 類加載核心原理。

1.?Java 字節碼文件與類加載機制

1.1 Java 虛擬機的組成

Java 虛擬機主要分為以下幾個組成部分:

  • 類加載子系統:核心組件是類加載器,負責將字節碼文件中的內容加載到內存中

  • 運行時數據區JVM 管理的內存創建出來的對象、類的信息等內容都會放在這塊區域中。

  • 執行引擎:包含即時編譯器、解釋器、垃圾回收器。執行引擎使用解釋器將字節碼指令解釋成機器碼,使用即時編譯器優化性能,使用垃圾回收器回收不再使用的對象。

  • 本地接口:調用本地使用 C/C++ 編譯好的方法,本地方法在 Java 中聲明時,都會帶上native關鍵字。

1.2 字節碼文件的組成

1.2.1 以正確的姿勢打開文件

字節碼文件中保存了源代碼編譯之后的內容,以二進制的方式存儲,無法直接用記事本打開閱讀。通過 NotePad++ 使用十六進制插件查看 class 文件:

無法解讀出文件里包含的內容,推薦使用 jclasslib 工具查看字節碼文件。

1.2.2 字節碼文件的組成

字節碼文件總共可以分為以下幾個部分:

  1. 基礎信息:魔數、字節碼文件對應的 Java 版本號、訪問標識、父類和接口信息

  2. 常量池:保存了字符串常量、類或接口名、字段名,主要在字節碼指令中使用

  3. 字段:當前類或接口聲明的字段信息

  4. 方法:當前類或接口聲明的方法信息,核心內容為方法的字節碼指令

  5. 屬性:類的屬性,比如源碼的文件名、內部類的列表等

1.2.2.1 基本信息

基本信息包含了 jclasslib 中能看到的 “一般信息” 相關內容,具體如下:

Magic 魔數

每個 Java 字節碼文件的前四個字節是固定的,用 16 進制表示為0xcafebabe文件無法通過擴展名確定類型(擴展名可隨意修改)軟件會通過文件頭(前幾個字節)校驗類型,不支持則報錯。

常見文件格式的校驗方式如下:

文件類型字節數文件頭
JPEG (jpg)3FFD8FF
PNG (png)489504E47(文件尾也有要求)
bmp2424D
XML (xml)53C3F786D6C
AVI (avi)441564920
Java 字節碼文件 (.class)4CAFEBABE

Java 字節碼文件的文件頭稱為 magic 魔數,Java 虛擬機會校驗字節碼文件前四個字節是否為0xcafebabe,若不是則無法正常使用,會拋出錯誤。

主副版本號

主副版本號指編譯字節碼文件時使用的 JDK 版本號:

  • 主版本號:標識大版本號,JDK1.0-1.1 使用 45.0-45.3,JDK1.2 為 46,之后每升級一個大版本加 1;1.2 之后大版本號計算方法為 "主版本號 – 44",例如主版本號 52 對應 JDK8。

  • 副版本號:主版本號相同時,用于區分不同版本,一般只需關注主版本號。

版本號的作用是判斷當前字節碼版本與運行時 JDK 是否兼容。若用較低版本 JDK 運行較高版本 JDK 編譯的字節碼文件,會顯示錯誤:

類文件具有錯誤的版本 52.0,應為 50.0,請刪除該文件或確保該文件位于正確的類路徑子目錄中。

解決兼容性問題的兩種方案:

其他基礎信息

其他基礎信息包括訪問標識、類和接口索引,具體說明如下:

名稱作用
訪問標識標識是類 / 接口 / 注解 / 枚舉 / 模塊;標識 public、final、abstract 等訪問權限
類、父類、接口索引通過這些索引可找到類、父類、接口的詳細信息
1.2.2.2 常量池

字節碼文件中常量池的作用是避免相同內容重復定義,節省空間。例如,代碼中編寫兩個相同的字符串 “我愛北京天安門”,字節碼文件及后續內存使用時只需保存一份,將該字符串及字面量放入常量池即可實現空間節省。

常量池中的數據都有編號(從 1 開始),例如 “我愛北京天安門” 在常量池中的編號為 7,字段或字節碼指令中通過編號 7 可快速找到該字符串。字節碼指令中通過編號引用常量池的過程稱為符號引用,示例如下:

  • 字節碼指令:ldc #7(符號引用編號 7 對應的字符串)

  • 常量池:編號 7 對應數據 “我愛北京天安門”

為什么需要符號引用?

編譯期(如?javac?編譯?.java?為?.class)根本不知道:

  • 被引用的類 / 方法在運行時會被加載到內存的哪個位置(內存地址由 JVM 動態分配);
  • 同一資源在不同 JVM、不同操作系統中的內存地址可能完全不同。

符號引用通過 “延遲綁定” 解決這個問題:編譯期只記錄 “要引用什么”,等到運行期類加載的 “解析階段”,JVM 再根據符號引用的信息,在內存中找到對應的資源,將其轉換為 “直接引用”(即內存地址)。

1.2.2.3 字段

字段中存放當前類或接口聲明的字段信息,包含字段的名字描述符(字段類型:int,long),訪問標識(修飾符:public、static、final 等)

1.2.2.4 方法

字節碼中的方法區域是存放字節碼指令的核心位置,字節碼指令的內容存放在方法的 Code 屬性中。例如,分析以下代碼的字節碼指令:

要理解字節碼指令執行過程,需先了解操作數棧局部變量表

  • 操作數棧存放臨時數據的棧式結構,先進后出

  • 局部變量表存放方法的局部變量(含參數、方法內定義的變量)

1. iconst_0:將常量 0 放入操作數棧,此時棧中只有 0。

2. istore_1:從操作數棧彈出棧頂元素(0),放入局部變量表 1 號位置編譯期確定為局部變量 i 的位置),完成 i 的賦值

3. iload_1:將局部變量表 1 號位置的數據(0)放入操作數棧,此時棧中為 0。

4. iconst_1:將常量 1 放入操作數棧,此時棧中有 0 和 1。

5. iadd:將操作數棧頂部兩個數據(0 和 1)相加,結果 1 放入操作數棧,此時棧中只有 1。

6. istore_2:從操作數棧彈出 1,放入局部變量表 2 號位置(局部變量 j 的位置)。

7. return:方法結束并返回。

同理,可分析i++++i的字節碼指令差異:

i++ 字節碼指令:iinc 1 by 1將局部變量表 1 號位置值加 1,實現 i++ 操作。

++i 字節碼指令:僅調整了iinciload_1的順序。

面試題int i = 0; i = i++; 最終 i 的值是多少?

:答案是 0。通過字節碼指令分析:i++ 先將 0 取出放入臨時操作數棧,接著對 i 加 1(i 變為 1),最后將操作數棧中保存的臨時值 0 放入 i,最終 i 為 0。

1.2.2.5 屬性

屬性主要指類的屬性,如源碼的文件名、內部類的列表等。例如,在 jclasslib 中查看 SimpleClass 的屬性,會顯示 SourceFile 屬性:

1.2.3 玩轉字節碼常用工具

1.2.3.1 javap

javap 是 JDK 自帶的反編譯工具,可通過控制臺查看字節碼文件內容,適合在服務器上使用

  • 查看所有參數:直接輸入javap

  • 查看具體字節碼信息:輸入javap -v 字節碼文件名稱

  • 若為 jar 包:需先使用jar –xvf jar包名稱命令解壓,再查看內部 class 文件。

?

1.2.3.2 jclasslib 插件

jclasslib 有 Idea 插件版本,開發時使用可在代碼編譯后實時查看字節碼文件內容。

1. 打開 Idea 的插件頁面,搜索 “jclasslib Bytecode Viewer” 并安裝。

2. 選中要查看的源代碼文件,選擇 “視圖(View)- Show Bytecode With Jclasslib”,右側會展示對應字節碼文件內容。

3. 文件修改后需重新編譯,再點擊刷新按鈕查看最新字節碼。

1.2.3.3 Arthas

Arthas 是一款線上監控診斷產品,可實時查看應用 load、內存、gc、線程狀態信息,且能在不修改代碼的情況下診斷業務問題,提升線上問題排查效率。

安裝方法

1. 將下載好的 arthas-boot.jar 文件復制到任意工作目錄。

2. 使用java -jar arthas-boot.jar啟動程序。

3. 輸入需要 Arthas 監控的進程 ID(啟動后會列出當前運行的 Java 進程)。

???

常用命令

dump:將字節碼文件保存到本地。

示例:將java.lang.String的字節碼文件保存到/tmp/output目錄:

jad:將類的字節碼文件反編譯成源代碼,用于確認服務器上的字節碼是否為最新。

示例:反編譯demo.MathGame并顯示源代碼

1.3 類的生命周期

類的生命周期描述了一個類加載、使用、卸載的整個過程,整體分為:

  1. 加載(Loading)

  2. 連接(Linking):包含驗證、準備、解析三個子階段

  3. 初始化(Initialization)

  4. 使用(Using)

  5. 卸載(Unloading)

類加載本身是一個過程,這個過程又細分為多個階段,包含加載,連接和初始化階段

1.3.1 加載階段

1. 加載階段第一步:類加載器根據類的全限定名,通過不同渠道以二進制流的方式獲取字節碼信息,程序員可通過 Java 代碼拓展渠道,常見渠道如下:

2. 類加載器加載完類后,Java 虛擬機會將字節碼中的信息保存到方法區,生成一個InstanceKlass對象,該對象保存類的所有信息(含實現多態的虛方法表等)。

3. Java 虛擬機同時會在上生成與方法區中數據類似的java.lang.Class對象,作用是在 Java 代碼中獲取類的信息,以及存儲靜態字段的數據(JDK8 及之后)。

步驟 1:類的 “來源獲取”

類的字節碼可以從多種來源被加載,如圖 1 所示:

  • 本地文件:最常見的情況,類的.class文件存儲在本地磁盤(如項目的classes目錄、jar包中),類加載器從本地文件系統讀取這些字節碼文件。
  • 網絡傳輸:在分布式應用(如 Applet、遠程服務調用)中,類的字節碼可通過網絡(如 HTTP、RPC)從遠程服務器傳輸到本地 JVM。
  • 動態代理生成:運行時通過字節碼生成庫(如 JDK 動態代理、CGLIB)動態生成類的字節碼,無需預先存在物理文件。

步驟 2:類加載器(ClassLoader)的 “加載動作”

類加載器(如圖 1 右側的ClassLoader)是加載階段的核心執行者,它的工作是:

  • 根據類的 “全限定名”(如java.lang.String),找到對應的字節碼數據。

? ? ? ?JVM 不僅要加載我們自己寫的應用類,還必須加載像java.lang.String這樣的核心類

  • 將字節碼數據以二進制流的形式讀取到 JVM 中

步驟 3:生成InstanceKlass對象(方法區存儲類元數據)

如圖 2 所示,JVM 在方法區生成一個InstanceKlass對象:

  • InstanceKlass是 JVM 內部用于表示類的核心數據結構,包含類的全部元數據
    • 基本信息:類的訪問修飾符(public、final 等)、類名、父類、接口等。
    • 常量池:存儲類中用到的常量(如字符串、符號引用等)。
    • 字段(Field):類中定義的成員變量信息。
    • 方法:類中定義的方法信息(包括方法名、參數、返回值、字節碼指令等)。
    • 虛方法表:支持多態的關鍵結構,存儲方法的動態調用入口。

步驟 4:生成java.lang.Class對象(堆中供開發者訪問)

如圖 3、圖 4 所示:

  • JVM 在堆區生成一個java.lang.Class對象,這個對象是開發者(Java 代碼)能直接訪問的 “類的鏡像”。
  • Class對象與方法區的InstanceKlass對象關聯Class對象中保存了訪問InstanceKlass的 “入口”,但屏蔽了底層復雜的元數據細節。

步驟 5:開發者與Class對象的交互(訪問控制)

如圖 5 所示:

  • 開發者無需直接操作方法區的InstanceKlass(包含 JVM 內部實現的敏感 / 復雜信息)。
  • 開發者只需通過堆中的Class對象,就能獲取類的公開可訪問信息(如通過Class.getMethods()獲取方法、Class.getFields()獲取字段等)。【反射】
  • 這種設計既讓開發者能便捷地反射(Reflection)操作類,又由 JVM 控制了訪問范圍(避免開發者直接篡改方法區的核心元數據)。

1.3.2 連接階段

連接階段分為三個子階段:

驗證(Verification)

驗證的主要目的是檢測 Java 字節碼文件是否遵守《Java 虛擬機規范》的約束,無需程序員參與,主要包含四部分(具體詳見《Java 虛擬機規范》):

  1. 文件格式驗證:如文件是否以0xCAFEBABE開頭,主次版本號是否滿足要求。??

  1. 元信息驗證:例如類必須有父類(super 不能為空)。

  2. 語義驗證:驗證程序執行指令的語義,如方法內指令跳轉至不正確的位置。

  3. 符號引用驗證:例如是否訪問了其他類中 private 的方法。

JDK8 源碼中對版本號的驗證邏輯如下:

編譯文件主版本號不高于運行環境主版本號;若相等,副版本號不超過運行環境副版本號。

準備(Preparation)

準備階段為靜態變量(static)分配內存并設置初值

不同數據類型的初值如下:

解析(Resolution)

解析階段主要是將常量池中的符號引用替換成指向內存的直接引用

  • 符號引用:字節碼文件中使用編號訪問常量池中的內容

  • 直接引用:使用內存地址訪問具體數據,無需依賴編號。

1.3.3 初始化階段

初始化階段會執行字節碼文件中clinit(class init,類的初始化)方法的字節碼指令,包含靜態代碼塊中的代碼并為靜態變量賦值。

1. iconst_1:將常量 1 放入操作數棧。

2. putstatic #2:彈出操作數棧中的 1,放入堆中靜態變量value的位置#2指向常量池中的value,解析階段已替換為變量地址),此時value=1

3. iconst_2:將常量 2 放入操作數棧。

4. putstatic #2:彈出 2,更新value為 2。

5. returnclinit方法執行結束,最終value=2

觸發類初始化的場景

clinit 不執行的情況
  1. 無靜態代碼塊且無靜態變量賦值語句。

  2. 有靜態變量的聲明,但沒有賦值語句(如public static int a;)。

  3. 靜態變量的定義使用 final 關鍵字(這類變量在準備階段直接初始化)。

面試題 1

分析步驟

步驟 1:類加載時執行靜態代碼塊

當 JVM 首次加載Test1類時,會執行靜態代碼塊(被static修飾的代碼塊)。靜態代碼塊在類加載階段執行,且只執行一次(無論創建多少個類的實例,靜態代碼塊都只執行一次)。

所以,程序啟動后,JVM 加載Test1類,首先執行static塊中的代碼:此時輸出:D

步驟 2:執行main方法中的代碼

main方法是程序入口,加載完類后,執行main方法內的代碼:

  • 第一行:System.out.println("A");?→ 輸出:A
  • 第二行:new Test1();?→ 創建Test1的實例,觸發實例初始化
  • 第三行:new Test1();?→ 再次創建Test1的實例,再次觸發實例初始化

步驟 3:實例初始化的順序(重點)

每次創建Test1實例時,實例初始化的順序是:

  1. 執行實例初始化塊(類中直接用{}包裹的代碼塊);
  2. 執行構造方法

所以,每次new Test1()時,執行順序為:

  • 實例初始化塊:System.out.println("C");?→ 輸出:C
  • 構造方法:System.out.println("B");?→ 輸出:B

兩次new Test1()的輸出

第一次new Test1()

  • 實例初始化塊輸出:C
  • 構造方法輸出:B

第二次new Test1()

  • 實例初始化塊再次輸出:C
  • 構造方法再次輸出:B

最終輸出順序
D(靜態代碼塊,類加載時執行)→?Amain方法第一行)→?C(第一次實例的初始化塊)→?B(第一次實例構造方法)→?C(第二次實例的初始化塊)→?B(第二次實例構造方法)

面試題 2

分析步驟

  1. 調用new B02()創建對象,需初始化 B02,優先初始化父類 A02。

  2. 執行 A02 的初始化代碼,a賦值為 1。

  3. 執行 B02 的初始化代碼,a賦值為 2。

  4. 輸出B02.a,結果為 2。

變化:若注釋new B02();,僅訪問B02.a(父類 A02 的靜態變量),則只初始化父類 A02,a=1,輸出結果為 1。

1.4 類加載器

1.4.1 什么是類加載器

類加載器(ClassLoader)是 Java 虛擬機提供給應用程序,用于實現獲取類和接口字節碼數據的技術。類加載器僅參與加載過程中 “字節碼獲取并加載到內存” 這一部分,具體流程如下:

  1. 類加載器通過二進制流獲取字節碼文件內容。

  2. 將獲取的數據交給 Java 虛擬機。

  3. 虛擬機會在方法區生成InstanceKlass對象,在堆上生成java.lang.Class對象,保存字節碼信息。

1.4.2 類加載器的分類

JDK8 及之前的默認類加載器

JDK8 及之前版本中,默認類加載器有三種,其關系如下:

  • 啟動類加載器(Bootstrap):無父類加載器,加載 Java 最核心的類

  • 擴展類加載器(Extension):父類加載器為啟動類加載器,允許擴展 Java 中通用的類

  • 應用程序類加載器(Application):父類加載器為擴展類加載器,加載應用使用的類

可通過 Arthas 的classloader命令查看類加載器信息

1.4.3 啟動類加載器

  • 實現方式:由 Hotspot 虛擬機提供,使用 C++ 編寫。

  • 默認加載路徑:Java 安裝目錄/jre/lib下的類文件(如 rt.jar、tools.jar、resources.jar 等)。

  • 擴展示例:-Xbootclasspath/a:D:/jvm/jar/classloader-test.jar

說明:String類由啟動類加載器加載,但 JDK8 中啟動類加載器用 C++ 編寫,Java 代碼中無法直接獲取,故返回 null。

1.4.4 擴展類加載器和應用程序類加載器

擴展類加載器

擴展類加載器加載用戶 jar 包示例

  • 擴展示例:-Djava.ext.dirs="C:\Program Files\Java\jdk1.8.0\_181\jre\lib\ext;D:\jvm\jar"

應用程序類加載器

應用程序類加載器會加載classpath下的類文件,默認加載的是項目中的類以及通過maven引入的第三方jar包中的類。

  • 默認加載路徑:classpath 下的類文件(項目中的類、maven 引入的第三方 jar 包中的類)。

  • 說明:項目類和第三方依賴類均由應用程序類加載器加載。

可通過 Arthas 的classloader -c 類加載器hash值?查看加載路徑

1.5 雙親委派機制

雙親委派機制指:當一個類加載器接收到加載類的任務時,會自底向上查找是否已加載,再由頂向下嘗試加載

類加載器的父子關系

詳細流程

1. 類加載器接收到加載任務后,先檢查自身是否已加載該類,若已加載則直接返回。

2. 若未加載,將任務委派給父類加載器,父類加載器重復步驟 1-2。

3. 若父類加載器(直至啟動類加載器)均未加載,且啟動類加載器無法加載(類不在其加載路徑),則由擴展類加載器嘗試加載。

4. 若擴展類加載器也無法加載,由應用程序類加載器嘗試加載。

案例分析

案例 1:類在啟動類加載器路徑中

假設com.itheima.my.A在啟動類加載器加載目錄(如/jre/lib),應用程序類加載器接收到加載任務:

1. 應用程序類加載器未加載過A,委派給父類(擴展類加載器)。

2. 擴展類加載器未加載過A,委派給父類(啟動類加載器)。

3. 啟動類加載器已加載過A,直接返回。

案例 2:類在擴展類加載器路徑中

假設com.itheima.my.B在擴展類加載器加載目錄(如/jre/lib/ext),應用程序類加載器接收到加載任務:

1. 應用程序類加載器未加載過B,委派給擴展類加載器。

2. 擴展類加載器未加載過B,委派給啟動類加載器。

3. 啟動類加載器未加載過BB不在其加載路徑,委派給擴展類加載器。

4. 擴展類加載器加載B成功,返回。

補充問題:

雙親委派機制的作用

  1. 保證類加載安全性:避免惡意代碼替換 JDK 核心類庫(如java.lang.String),確保核心類庫完整性和安全性。

  2. 避免重復加載:同一類不會被多個類加載器重復加載。

如何指定類加載器加載類

在 Java 中可通過兩種方式主動加載類

1.使用Class.forName方法:使用當前類的類加載器加載指定類,示例:

Class<?> clazz = Class.forName("com.itheima.my.A");

2.獲取類加載器,調用loadClass方法:指定類加載器加載,示例:

// 獲取應用程序類加載器
?
ClassLoader classLoader = Demo1.class.getClassLoader();
?
// 使用應用程序類加載器加載com.itheima.my.A
?
Class<?> clazz = classLoader.loadClass("com.itheima.my.A");
  • Class.forName()java.lang.Class類的靜態方法,加載指定全類名的類時會主動執行類的初始化(如靜態代碼塊、靜態變量初始化),常用于反射或需觸發類初始化的場景。
  • loadClass():java.lang.ClassLoader類的實例方法,僅將類加載到 JVM 但默認不進行初始化,主要用于類加載器自定義實現與類加載控制。
  • 區別:二者均可能拋出ClassNotFoundException,核心區別在于是否主動初始化類及調用主體、適用場景不同。

面試題

:若一個類重復出現在三個類加載器的加載位置,由誰加載?

:啟動類加載器加載,雙親委派機制中啟動類加載器優先級最高。


:String 類能覆蓋嗎?在項目中創建java.lang.String類,會被加載嗎?

:不能。啟動類加載器會優先加載rt.jar中的java.lang.String類,項目中的String類不會被加載。


:類的雙親委派機制是什么?

:當類加載器加載類時,自底向上查找是否已加載,若均未加載則由頂向下嘗試加載。應用程序類加載器父類是擴展類加載器,擴展類加載器父類是啟動類加載器。好處是保證核心類庫安全、避免重復加載。

1.6 打破雙親委派機制

打破雙親委派機制歷史上有三種方式,本質上僅第一種真正打破:

  1. 自定義類加載器并重寫loadClass方法(如 Tomcat 實現應用間類隔離)。

  2. 線程上下文類加載器(如 JDBC、JNDI 使用)。

  3. Osgi 框架的類加載器(歷史方案,目前很少使用)。

自定義類加載器

背景

原理

ClassLoader核心方法

1. public Class<?> loadClass(String name):類加載入口,實現雙親委派機制,內部調用findClass

2. protected Class<?> findClass(String name):子類實現,獲取二進制數據并調用defineClass

3. protected final Class<?> defineClass(String name, byte[] b, int off, int len):校驗類名,調用虛擬機底層方法將字節碼加載到內存。

4. protected final void resolveClass(Class<?> c):執行類生命周期的連接階段。

1. 入口方法:

2. 再進入看下:

如果查找都失敗,進入加載階段,首先會由啟動類加載器加載,這段代碼在findBootstrapClassOrNull中。如果失敗會拋出異常,父類加載器加載失敗就會拋出異常,回到子類加載器的這段代碼,這樣就實現了加載并向下傳遞。

3. 最后根據傳入的參數判斷是否進入連接階段:

自定義類加載器實現

重新實現下面的核心代碼(loadclass)就可以打破雙親委派機制

package classloader.broken;//package com.itheima.jvm.chapter02.classloader.broken;import org.apache.commons.io.IOUtils;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.ProtectionDomain;
import java.util.regex.Matcher;/*** 打破雙親委派機制 - 自定義類加載器*/public class BreakClassLoader1 extends ClassLoader {private String basePath;private final static String FILE_EXT = ".class";//設置加載目錄public void setBasePath(String basePath) {this.basePath = basePath;}//使用commons io 從指定目錄下加載文件private byte[] loadClassData(String name)  {try {String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);try {return IOUtils.toByteArray(fis);} finally {IOUtils.closeQuietly(fis);}} catch (Exception e) {System.out.println("自定義類加載器加載失敗,錯誤原因:" + e.getMessage());return null;}}//重寫loadClass方法@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {//如果是java包下,還是走雙親委派機制if(name.startsWith("java.")){return super.loadClass(name);}//從磁盤中指定目錄下加載byte[] data = loadClassData(name);//調用虛擬機底層方法,方法區和堆區創建對象return defineClass(name, data, 0, data.length);}public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {//第一個自定義類加載器對象BreakClassLoader1 classLoader1 = new BreakClassLoader1();classLoader1.setBasePath("D:\\lib\\");Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");//第二個自定義類加載器對象BreakClassLoader1 classLoader2 = new BreakClassLoader1();classLoader2.setBasePath("D:\\lib\\");Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");System.out.println(clazz1 == clazz2);Thread.currentThread().setContextClassLoader(classLoader1);System.out.println(Thread.currentThread().getContextClassLoader());System.in.read();}
}

? ? ? 問題一:為什么這段代碼打破了雙親委派機制?

雙親委派機制的核心是:類加載器在加載類時,會先委托給父類加載器加載,只有父類加載器無法加載時,才自己嘗試加載

而這段代碼通過重寫?loadClass()?方法打破了這一機制:

  • 對于非?java.?開頭的類(如自定義類?com.itheima.my.A),代碼直接跳過父類加載器,自己從指定目錄加載類(loadClassData()?方法讀取字節碼)
  • 只有?java.?開頭的核心類才遵循雙親委派(調用?super.loadClass(name)?讓父類加載器處理)

正常情況下,loadClass()?方法的默認實現會先委托父類加載器,而這里重寫后改變了這一流程,因此打破了雙親委派機制。

問題二:兩個自定義類加載器加載相同限定名的類,不會沖突嗎?

不會沖突,原因是:

在 JVM 中,一個類的唯一性由「類的全限定名 + 加載它的類加載器」共同決定。即:

  • 即使兩個類的全限定名完全相同,只要由不同的類加載器加載,JVM 會認為它們是兩個不同的類
  • 代碼中?classLoader1?和?classLoader2?是兩個不同的實例(不同的類加載器對象),因此它們加載的?com.itheima.my.A?會被視為兩個不同的類
  • 這也是為什么代碼中?clazz1 == clazz2?的輸出結果為?false

這種特性保證了即使類名相同,只要加載器不同,就不會產生沖突,這也是 Java 類加載機制的重要設計。

關鍵說明

自定義類加載器的父類:默認情況下,自定義類加載器的父類加載器是應用程序類加載器(AppClassLoader),因ClassLoader構造方法中parentgetSystemClassLoader()(返回AppClassLoader)設置。

線程上下文類加載器

背景

雙親委派機制核心:類加載器在加載類時,優先委托父類加載器去加載。只有當父類加載器無法加載(比如父類加載器的搜索路徑里沒有該類),當前類加載器才會嘗試自己加載。

原理

SPI?是 “約定好的配置方式”,讓核心庫能找到第三方實現的類名。

線程上下文類加載器?是 “工具”,讓核心庫(由父加載器加載)能突破雙親委派,用子加載器(應用程序類加載器)去加載第三方庫的類。

SPI 機制

SPI 機制通過在 jar 包META-INF/services目錄下放置接口名文件(如java.sql.Driver),文件中寫入實現類全限定名(如com.mysql.cj.jdbc.Driver),從而找到接口實現類。

JDBC 加載驅動流程

  1. 啟動類加載器加載DriverManager

  2. DriverManager初始化時,調用LoadInitialDrivers方法,通過 SPI 機制加載META-INF/services/java.sql.Driver中的實現類。

  3. SPI 機制使用線程上下文類加載器(應用程序類加載器)加載 MySQL 驅動類(com.mysql.cj.jdbc.Driver)。

  4. 驅動類初始化時,調用DriverManager.registerDriver(new Driver()),完成注冊。

JDBC案例中真的打破了雙親委派機制嗎?

最早這個論點提出是在周志明《深入理解Java虛擬機》中,他認為打破了雙親委派機制,這種由啟動類加載器加載的類,委派應用程序類加載器去加載類的方式,所以打破了雙親委派機制。

但是如果我們分別從DriverManager以及驅動類的加載流程上分析,JDBC只是在DriverManager加載完之后,通過初始化階段觸發了驅動類的加載,類的加載依然遵循雙親委派機制。

所以我認為這里沒有打破雙親委派機制,只是用一種巧妙的方法讓啟動類加載器加載的類,去引發的其他類的加載。

Osgi 框架的類加載器

Osgi 是模塊化框架,實現了同級類加載器委托加載,還支持熱部署(服務不停止時動態更新字節碼)。但目前使用較少,此處不展開。

熱部署案例:Arthas 不停機修復線上問題

注意事項

  1. 程序重啟后,字節碼恢復,需將新 class 文件放入 jar 包更新。

  2. retransform不能添加方法 / 字段,不能更新正在執行的方法。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/96634.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/96634.shtml
英文地址,請注明出處:http://en.pswp.cn/web/96634.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

多源最短路(Floyd算法

多源最短路簡介 多源最短路算法用于解決圖中任意兩節點間最短路徑的問題&#xff0c;廣泛應用于交通網絡、社交關系分析、路由優化等場景。與單源最短路&#xff08;如Dijkstra&#xff09;不同&#xff0c;它一次性計算所有節點對的最短距離&#xff0c;適合需要全局路徑規劃的…

【攻防實戰】記一次攻防實戰全流程

那天我向眾神祈禱&#xff0c;最后回答我的卻只有掙扎十年依舊不甘的自己&#xff01;成功究竟是饋贈還是償還。 前言 網絡安全技術學習&#xff0c;承認??的弱點不是丑事&#xff0c;只有對原理了然于?&#xff0c;才能突破更多的限制。 擁有快速學習能力的安全研究員&…

Anaconda配置環境變量和鏡像

Anaconda配置環境變量和鏡像 下載失敗就是開了梯子 Anaconda 作用&#xff1a;包管理&#xff08;集中&#xff0c;有序&#xff09;和環境管理&#xff08;版本切換&#xff09;使用conda命令對虛擬環境創建、刪除自帶python解釋器pip&#xff08;python自帶的包管理工具&…

給定單詞倒排

實現代碼&#xff1a;public static void main(String[] args) {Scanner scanner new Scanner(System.in);// 輸入的字符串String input scanner.nextLine();// 存儲單詞List<String> words new ArrayList<>();// 存儲當前單詞StringBuilder currentWord new S…

IO進程——進程引入、進程函數接口

一、引入1、進程&程序1.1 程序編譯好的可執行的文件存放在磁盤上的指令和數據的有序集合&#xff08;文件&#xff09;程序是靜態的&#xff0c;沒有任何執行的概念1.2 進程一個獨立的可調度的任務執行一個程序所分配的資源的總稱進程是程序執行的一次過程進程是動態的&…

周末游戲推薦:安卓端俄羅斯方塊,經典與創新的結合

前段時間&#xff0c;每到周末我都會給大家推薦一些離線的經典游戲&#xff0c;原本打算將這個傳統一直延續下去。然而&#xff0c;我實在找不到足夠好用且無廣告的游戲了。有些游戲剛開始用的時候還不錯&#xff0c;但用著用著就開始頻繁彈出廣告&#xff0c;這讓我實在不敢向…

《用 Scikit-learn 構建 SVM 分類模型:從原理到實戰的全流程解析》

《用 Scikit-learn 構建 SVM 分類模型:從原理到實戰的全流程解析》 一、引言:為什么選擇 SVM? 在機器學習的眾多算法中,支持向量機(SVM)以其強大的分類能力和良好的泛化性能,在文本分類、人臉識別、醫學診斷等領域廣泛應用。尤其在中小規模數據集上,SVM 往往能提供比…

一文學會CMakeLists.txt: CMake現代C++跨平臺工程化實戰

你能學到什么&#xff1f;朋友們好久不見&#xff0c;我是alibli&#xff0c;好久沒有更新博客了。今天本人將通過構造一個實際的虛擬小項目&#xff0c;來讓你徹底掌握CMake跨平臺工程構建&#xff0c;學會CMakeLists.txt語法。該項目實現了一個簡單的平方、立方的計算程序&am…

高并發場景下限流算法實踐與性能優化指南

高并發場景下限流算法實踐與性能優化指南 在大規模并發訪問環境中&#xff0c;合理的限流策略能保護后端服務穩定運行&#xff0c;避免系統因瞬時高并發導致資源耗盡或崩潰。本文將從原理出發&#xff0c;深入解析幾種主流限流算法&#xff0c;并結合Java和Redis給出完整可運行…

Vue3應用執行流程詳解

精確化的完整執行流程 (以 Vite Vue3 SPA 為例)整個過程可以分為兩部分&#xff1a;首次訪問的“冷啟動”和后續的Vue應用接管。第一部分&#xff1a;首次訪問與頁面加載客戶端&#xff1a;發送請求用戶打開瀏覽器&#xff0c;輸入 URL&#xff08;如 http://localhost:5173&a…

Redis 持久化與高可用實踐(RDB / AOF / Sentinel / Cluster 全解析)

這篇是我把幾套生產環境踩坑與復盤整理成的一份“從 0 到 1 長期可維護”的實踐文。目標是&#xff1a;明確策略、給出默認可用的配置模板、把常見坑一次講透。 適用場景&#xff1a;新項目選型、老項目穩定性加固、從單機遷移到 HA/Cluster、應對數據安全與故障切換要求。目錄…

Linux內核的PER_CPU機制

參考書《Linux內核模塊開發技術指南》 1.原理 在多核CPU的情況下&#xff0c;為了提高CPU并發執行的效率&#xff0c;對于某些不是必須要在核間進行同步訪問的資源&#xff0c;可以為每一個CPU創建一個副本&#xff0c;讓每個CPU都訪問自身的數據副本&#xff0c;而不是通過加鎖…

VSCode 的百度 AI編程插件

VSCode 的百度 AI編程插件主要是 Baidu Comate&#xff08;文心快碼&#xff09;&#xff0c;這是一款基于文心大模型的新一代編碼輔助工具&#xff0c;旨在提升開發者的編碼效率&#xff0c;讓寫代碼變得更簡單。以下是關于 Baidu Comate 的詳細介紹&#xff1a; 一、功能特點…

阿里云監控使用

阿里云的云監控服務&#xff08;CloudMonitor&#xff09;是一款簡單易用、功能強大的監控工具&#xff0c;主要用來幫助用戶實時監控阿里云上的各種資源&#xff08;比如服務器、數據庫、網絡等&#xff09;&#xff0c;并在出現問題時及時發出警報&#xff0c;確保業務穩定運…

嵌入式C語言-關鍵字typedef

定義和作用 typedef是C/C中的一個關鍵字&#xff0c;作用是為現有的數據類型&#xff08;int 、char 、flaot等&#xff09;創建新的別名&#xff0c;其目的是為了方便閱讀和理解代碼。 用法 typedef 原有類型名 新類型名;基本類型創建別名 typedef unsigned char uint8_t; typ…

【混合開發】【大前端++】Vue節點優化Dome之單節點輪播圖片播放視頻二

動圖更精彩 背景 Vue作為大前端開發頁面交互&#xff0c;在數字屏&#xff0c;智慧大屏等大屏幕開發過程中&#xff0c;輪播效果作為豐富的展示組件經常作為首選。但也因為這個組件的交互體驗很好&#xff0c;于是各種單點組件增加到輪播效果里。經過業務的擴展&#xff0c;人…

前端開發核心技術與工具全解析:從構建工具到實時通信

覺得主包文章可以的,可以點個小愛心喲&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 主頁:一位搞嵌入式的 genius-CSDN博客 系列文章專欄: https://blog.csdn.net/m0_73589512/category_13028539.html 前端開發核心技術與工具全解…

GPT 系列論文 gpt3-4 175B參數 + few-shot + 多模態輸入 + RLHF + system

GPT&#xff0c;GPT-2&#xff0c;GPT-3 論文精讀【論文精讀】 GPT-4論文精讀 從1750億參數的文本預言家&#xff0c;到多模態的通用天才&#xff0c;OpenAI用兩次震撼世界的發布&#xff0c;重新定義了人工智能的可能性邊界。這份筆記將帶你深入GPT-3和GPT-4的核心突破&#…

.gitignore文件的作用及用法

目錄 ??.gitignore 文件的作用?? ??.gitignore 的基本語法?? ??Python 項目的 .gitignore 示例?? ??如何使用 .gitignore?? ??1. 創建 .gitignore 文件?? ??2. 編輯 .gitignore?? ??3. 檢查 Git 狀態?? ??常見問題?? ??Q1&#xff…

QEMU環境準備

QEMU環境準備 下載 qemu # qemu sudo apt install qemu-system-arm # gdb sudo apt install gdb-multiarchsudo apt-get update sudo apt-get install build-essential zlib1g-dev pkg-config libglib2.0-dev \libpixman-1-dev libfdt-dev ninja-build下載并自行編譯 qemu(可…