--- title: '類別 - 生命週期、ClassLoader' disqus: kyleAlien --- 類別 - 生命週期、ClassLoader === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile_cat: JVM 會為 Java 程式提供執行的環境,而 JVM 其中一個重要的任務就是 **管理類別、物件的生命週期**,這個小節則針對 **類** 說明 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**「類」的生命週期、ClassLoader 加載 | JVM 與 Class | Java 為例**](https://devtechascendancy.com/class-lifecycle_classloader-exploration_jvm/) ::: [TOC] ## 類加載 - 概述 類的生命週期如下圖;重點是 JVM 加載 class 文件加載過後 ^1.^ **二進制放置方法區**、^2.^ **創建一個(唯一)JVM 堆區** ```sequence JVM 環境->class文件: 加載 class 文件 JVM 環境->JVM 方法區: 二進位,放置 Java 方法區 JVM 環境->JVM 堆區: 放置一個 Class 物件! ``` ### JVM 生命週期 * JVM 生命週期如下 * **JVM 啟動**:JVM 的啟動是隨應用程式的啟動 當我們透過 Java 命令執行 Java 應用後,就會啟動一個 JVM(**一個應用對應一個虛擬機的實體**) * **JVM 結束**:JVM 的結束通常有以下方式 * 程式的正常結束 * 程式發生異常錯誤導致例外中止 * 執行 `System#exit()` 方法,主動退出 * JVM 運行的 OS 平台,但平台發生意外,導致應用關閉,JVM 也會結束 :::info * JVM 的生命週期會隨應用的啟動、關閉共同運行 ::: ## 類的生命週期 > 這裡說的是類!不是物件唷~ * 類加載入 JVM 至 卸載出 JVM 的過程稱為類的生命週期,而類的生命週期有 ==**5 個階段**== 1. **類加載**:找查加載 Class 文檔 (Class 文檔轉為 Class類) > * 根據**特定名稱找到類 or 界面的二進制字節流** > > * 將該二進制字節流代表的**靜態儲存結構** ==轉化到方法區== 的**運行時數據結構** > > * 在**內存中**生成一個代表這個類的 **java.lang.Class 對象**,**放置在方法區 (唯一一個 class)**,做為這個類的各種數據訪問入口 2. **類連結** (分三個階段) > * **驗證** : 確保引入的類型的正確性 > > * **準備** : 類的 static 字段(static 區塊、static Field)分配,並初始化 > > * **解析** : VM 將 **常量池** 內的符號引用替換為直接引用 3. **類初始化**:將類的變量初始化為正確值 4. **類使用** 5. **類卸載** > ![](https://i.imgur.com/P9Zg2Yy.png) :::danger * **類的加載時機?** 而一個類的加載時機又有分幾種(之後小節會提到),但有一個原則就是 **Java 應用 ++首次主動使用++ 這個類的時候**,該類就會加載 ::: ### 類別 - 載入 * **類(Class 文件)的載入是指**: 1. **方法區資料**:將 `.class` 檔中的二進位資料讀取到記憶體中,並存放在 JVM 執行時期的方法區中! 而載入類的方式並非只有直接透過檔案,只要載入 Class 文件格式的資料,使用其他方式也可以,像是: * 本地 `.class` 文件 * 網路傳輸進來的 2 進位 `.class` 文件 * 從 `ZIP`, `JAR` ... 或其他壓縮檔提取的`.class` 文件 * 從記憶體中動態創建 `.class` 文件並加載 2. **堆區資料**:在堆區建立該類相對應的 `java.lang.Class` 物件 ```sequence JVM 環境->JVM 方法區: 二進位,放置 Java 方法區 JVM 環境->JVM 堆區: 放置一個 Class 物件! ``` :::info * 而加載類的工具是啥?就是 **類加載器**(之後說明) ::: ### 類別 - 連結 * 連結就是將已經讀入記憶體的類別資訊(2 進位)**合併到虛擬機的運行時環境**;我們可以知道連結有 3 個步驟:**驗證、初始化準備、解析** 1. **驗證**(`Verifucation`): 保證被載入的類別有正確的 Class 資料結構!這個步驟可以確認以下事項 * **Class 資料結構**:確保內容格式是正確的 > 像是檢查 **Class 魔數** 就可以知道該文件是否是 Class 文檔 * **語意檢查**:以 Java 語言的規範角度去檢查是否語意正確 > final 類別不會有繼承、final 方法不會被覆蓋 * **ByteCode 驗證**:ByteCode 以方法來說,會透過 **操作碼的指令** 組成方法,在這個步驟會檢查操作碼的合法性 * **二進位相容性驗證**:確保相互參考的類別之間的 **協調一致** > 就像是加載的類如果沒有呼叫類別需要的方法時就會拋出 `NoSuchMethodError` 錯誤 ```sequence main->HelloWorld: 呼叫 HelloWorld#say 方法 類加載器->HelloWorld: 加載並驗證是否有 say 方法 ``` 2. **初始化準備**(`Prepartion`): * **記憶體空間**:為類的靜態變數、靜態區塊分配對應的記憶體空間 * **初始化**:賦予靜態變數初始值;這裡的初始化是針對記憶體的初始化 > 類似使用 C 語言的 `memset` 函數,將該記憶體區塊全部清為 0 ```java= public class PrepareStatic { // 初始化為 0 private static int apple; static { // 初始化為 0,尚未真正初始化! int banana = 10; } } ``` 3. **解析**(`Resolution`): JVM 會將類別(`.class`)的二進位資料中的 **符號參考替換為直接參考值**;所謂的 **符號是我們程式中撰寫的程式的參考(ref)** :::warning * 也就是將「符號引用」轉為「直接引用」(類似 C/C++ 中的重新定位過程) ::: ```java= class Person { // helloWorld 就是符號,同時也是參考 private final HelloWorld helloWorld = new HelloWorld(); void firstSay() { // say 也是符號 // 解析階段會將 HelloWorld#say // 轉換為 HelloWorld#say 放在方法區的位置的指標! helloWorld.say(); } } class HelloWorld { void say() { // do somthing... } } ``` ``` mermaid graph TD; Person解析-->發現Person#firstSay方法; 發現Person#firstSay方法-->找到HelloWorld方法區的say方法_替換為指標; 找到HelloWorld方法區的say方法_替換為指標-->HelloWorld#say方法; HelloWorld方法區-->HelloWorld#say方法; ``` ### 類別 - 初始化 > 這裡的初始化與 連結時期 的初始化不同,這裡的初始化尚未到建構函數(在堆區建構物件時才會呼叫到),**這裡針對類(方法區)做初始化** * **按照順序** 執行初始化 ```java= public class PrepareStatic { private static int apple; static { apple = 10; } // 由於按照順序,最後賦予值的設定將為最終設定 static { apple = 200; } public static void main(String[] args) { System.out.println(PrepareStatic.apple); } } ``` > ![](https://hackmd.io/_uploads/rk_7YkW6n.png) * **JVM 初始化一個類時基礎規則為**: * **按順序初始化**:如上所述 * **尚未加載**:如果目標類別尚未加載,那會先將該類加載(載入、連結、驗證)再初始化 ```java= class SubClz { static int tmp = 200; static { System.out.println("SubClz static block: " + tmp); } } public class PrepareStatic { public static void main(String[] args) { System.out.println("Sub Clz tmp: " + SubClz.tmp); } } ``` > ![](https://hackmd.io/_uploads/SJl8mnba2.png) * **父類尚未加載**:如果目標類有父類,並父類也尚未加載:那會先加載父類進方法區並建立堆區的 Class 訊息再初始化,再加載子類(目標類)最後出初始化子類 ```java= class ParentClz { static int tmp = 10; static { System.out.println("ParentClz static block: " + tmp); } } // 有父類 class SubClz extends ParentClz { static int tmp = 200; static { System.out.println("SubClz static block: " + tmp); } } public class PrepareStatic { public static void main(String[] args) { System.out.println("Sub Clz tmp: " + SubClz.tmp); } } ``` > ![](https://hackmd.io/_uploads/H1ZVmnb6n.png) ## 類 - 加載時機 我們前面有說到,類的加載是在我們 **初次 ++主動++ 使用類別時**,而這個初次主動 **主動又有分** 的,**並不是有使用到就算主動**; 主動使用到有分如下行為,這些行為 JVM 才會幫我們進行類加載 ### 啟動類別 * **啟動類別**,也就是擁有 Main 函數的類別,在呼叫 `java` 指令呼叫該類時,JVM 會先將其初始化 ```java= public class EntryClz { // 我們知道類加載後連結(初始化階段)會呼叫靜態區塊 // 以此,我們來就可以確認 EntryClz 是否被加載 static { System.out.println("Entry Clz has loaded."); } public static void main(String[] args) { System.out.println("Hello Entry Clz"); } } ``` * **測試**:java 命令呼叫 `EntryClz` 類,讓 JVM 加載該類,並執行內部的 `main` 方法 1. 使用 `javac` 將 `EntryClz.java` 編譯成 `EntryClz.class` 檔案(編譯指令如下) ```java= javac EntryClz.java ``` 2. 使用 `java` 執行 `EntryClz.class` 檔案(執行指令如下) ```java= java EntryClz ``` > ![](https://hackmd.io/_uploads/S1Wf0TW63.png) ### 創建物件 * 建立類的 instance 物件 也會觸發 JVM 加載一個類 > 最常見的就是使用 **`new` 關鍵字** 創建一個物件,而其中也 **包含透過反射創建一個類的 instacnce** 1. **`new` 關鍵字** 創建物件 ```java= public class NewInstance { public static void main(String[] args) { // 引用不會觸發類加載 MyClz myClz; System.out.println("Before create instance"); myClz = new MyClz(); System.out.println("After create instance"); } } class MyClz { static { System.out.println("MyClz has loaded."); } } ``` :::warning * 從結果可以發現:**引用不會導致類的加載**、初始化 (可以從 static 區塊,發現類是否有被加載) ::: > ![](https://hackmd.io/_uploads/S1PaCpW63.png) 2. **反射** 創建物件 ```java= public class NewInstance { public static void main(String[] args) throws InstantiationException, IllegalAccessException { System.out.println("Before create instance"); MyClz.class.newInstance(); System.out.println("After create instance"); } } class MyClz { static { System.out.println("MyClz has loaded."); } } ``` > ![](https://hackmd.io/_uploads/SkRc-C-an.png) ### 靜態元素 * **靜態元素** 包括如下兩種 * 呼叫 **靜態方法** ```java= class LoadStaticMethod { static { System.out.println("LoadStaticMethod loaded"); } // 呼叫會觸發加載 static void showMsg() { System.out.println("HelloWorld"); } } ``` > ![](https://hackmd.io/_uploads/HksyE0bph.png) * 存取類別的 **靜態 ++變數++** ```java= class LoadStaticField { // 存取會觸發加載 static int commonValue = 0; static { System.out.println("LoadStaticField loaded"); } } ``` > ![](https://hackmd.io/_uploads/BJM3XAZa3.png) :::warning * **編譯時期常數** `final static` 常數,這種常數又稱為 **編譯時期常數**;會在編譯期間直接做優化,將該常數在使用到的地方直接替代 > 訪問 Const Field 不會觸發 JVM 加載該類 ```java= class ConstField { static { System.out.println("ConstField loaded"); } // 呼叫這個 Const Field 不會觸發 JVM 加載該 Class static final boolean IS_DEBUG = true; } public class LoadByStatic { public static void main(String[] args) { System.out.println("Is debug: " + ConstField.IS_DEBUG); } } ``` > ![](https://hackmd.io/_uploads/HJSrEA-ph.png) 我們也可以透過 javac、javap 指令來查看 **常量得值,會被編譯器直接取代,不會呼叫該類**(這是一種編譯器的優化) > ![](https://hackmd.io/_uploads/S1gHB0-ph.png) :::danger * JVM 加載 編譯時期常數 不會為它在方法區創建一塊空間(編譯時期常數分配在其他區域) > 編譯時期常數分配在 Java「常數池」(Constant Pool)中,這裡的訪問效率更高 ::: ::: ### 反射呼叫方法 * 透過 Java 反射相關 API 操作類的方法(**因為反射會分析堆區中的 Class 物件、方法區的二進制碼,所以必須先載入類**) ```java= class ReflectionMethod { void showMsg() { System.out.println("Hello World, I am method."); } } public class LoadByReflection { public static void main(String[] args) throws InstantiationException, IllegalAccessException { ReflectionMethod.class.newInstance().showMsg(); } } ``` > ![](https://hackmd.io/_uploads/HyxO4LRbp2.png) * 這裡要提早提及一個有關於 ClassLoader 的蓋念,**使用 ClassLoader 加載並不會導致類的初始化**(**只載入到方法區,尚未載入堆區**) 1. 透過以下範例,可以觀察到,如果 **單純使用 ClassLoader 並不會導致類的初始化** ```java= class HelloWorld { static { System.out.println("Hello World class has loaded."); } } public class LoadByClassLoader { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader sysClzLoader = ClassLoader.getSystemClassLoader(); Class<?> clz = sysClzLoader.loadClass("HelloWorld"); System.out.println("Loader class success? " + (clz != null) + "\n"); System.out.println("Load class finish"); clz.newInstance(); } } ``` 2. 再看看以下範例,我們使用 `Class#forName` 函數來加載一個類,這時會發現 **`Class#forName` 會觸發類的初始化!** ```java= class HelloWorld { static { System.out.println("Hello World class has loaded."); } } public class LoadByReflection { public static void main(String[] args) throws ClassNotFoundException { Class.forName("HelloWorld"); } } ``` :::info * 從上面兩個範例可以知道,**JVM 加載類不等於類的初始化**,這是兩個概念!只有 **類被使用之時才會進行初始化** ClassLoader 加載進入 JVM 後不會立即進行初始化。類的初始化只有在需要時才會發生,即當你使用該類的實例、靜態方法、靜態變數等時 > ClassLoader 負責類生命週期的前兩個步驟,不一定會有類初始化 ::: ### 初始化子類 & 介面 * 初始化一個類別的子類,那父類也會一起被加載進來(並且先於子類);這裡說的初始化包含創建、呼叫 Static Field、Static Method * 接下來我們來看幾種狀況 1. **創建子類**:會觸發父類加載,再加載子類 ```java= class ParentClz { static int tmp = 10; static { System.out.println("ParentClz static block: " + tmp); } } class SubClz extends ParentClz { static int tmp = 200; static { System.out.println("SubClz static block: " + tmp); } } public class PrepareStatic { public static void main(String[] args) { System.out.println("Before instance Clz"); new SubClz(); System.out.println("After instance Clz"); } } ``` > ![](https://hackmd.io/_uploads/rkJx_AZ62.png) 2. **創建父類**:如果 **只單獨創建父類,那並不會觸發 JVM 加載子類** ```java= class ParentClz { static int tmp = 10; static { System.out.println("ParentClz static block: " + tmp); } } class SubClz extends ParentClz { static int tmp = 200; static { System.out.println("SubClz static block: " + tmp); } } public class PrepareStatic { public static void main(String[] args) { System.out.println("Before instance Clz"); new ParentClz(); System.out.println("After instance Clz"); } } ``` > ![](https://hackmd.io/_uploads/BJh7iCZTh.png) 3. **類只會加載一次**:**如果父類已經被加載,那 JVM 就不會再次加載** ```java= class ParentClz { static int tmp = 10; static { System.out.println("ParentClz static block: " + tmp); } } class SubClz extends ParentClz { static { System.out.println("First SubClz static block\n"); } } class SubClz2 extends ParentClz { static { System.out.println("Second static block\n"); } } public class PrepareStatic { public static void main(String[] args) { System.out.println("Before instance Clz"); new SubClz(); new SubClz2(); System.out.println("After instance Clz"); } } ``` > ![](https://hackmd.io/_uploads/S1gkhAb6h.png) * 在 JVM 規範中,JVM 初始化一個類時會要求同時初始化父類,但 **不會要求初始化介面** * 在初始化一個類別時,不會先初始化它所實作的介面 * 在初始化一個介面時,不會初始化它的父介面 :::warning * 只有當實際使用到介面的成員(如方法、字段)時,相關的介面才會被加載。 > 這可以降低複雜性、提高類加載的效率 ::: ## ClassLoader 概述 類是透過哪個工具載入的呢?**類別是透過 ClassLoader 加載**,主要有幾中加載器 * **內建類加載器**(JVM) * **啟動類別加載器** * **擴展類別載入器** * **系統類載入器** :::success * JVM 允許類別載入器(`ClassLoader`) **預料** 某個類的載入,而順便載入,如果載入失敗,則拋出錯誤 ::: ### JVM 內建類加載器 * JVM 有預設幾個內建類加載器: | 類加載器 | 特色 | 載入類 | 來源 | | - | - | - | - | | Bootstrap | **沒有父加載器**,實現依賴於底層作業系統,並且 **不繼承 ClassLoader 類** | **載入虛機的核心類別庫** (`java.lang.*`... 像是 Object 類) | 系統屬性 `sun.boot.class.path` | | Extension | **父加載器為 Bootstrap** | 放在 `jre/lib/ext` 目錄下的 jar 檔,會被自動加載 | 系統屬性 `java.ext.dirs` | | System | 又稱為應用類別載入器,**父加載器為 Extension** | 載入應用的類 | 系統屬性 `java.class.path` 或是 `classpath` 屬性 | 加載順序如下 ``` mermaid graph TD; Custom-->System; System-->Extension; Extenion-->Bootstrap; ``` > `Custom` 是使用者可自訂的類加載器 * 加載器驗證測試範例: * **Bootstrap** 加載器:會加載 java 自訂的核心類,像是 Object 類 ```java= public static void main(String[] args) throws ClassNotFoundException { Class<?> objClz = Class.forName("java.lang.Object"); System.out.println("Object classLoader: " + objClz.getClassLoader()); } ``` :::warning * **Bootstrap 返回的是 null** 從結果看來,Bootstrap 返回的是 null,這可以安全的保護 Bootstrap 不讓其他使用者使用 ::: > ![](https://hackmd.io/_uploads/rkdRx4Gph.png) ### 父委託機制 * **父委託機制**(也稱為 雙親委託機制),這是類加載實現的,類加載的核心概念入下: 1. 由使用者自己的類加載器,開始載入 2. 但實做載入的並非是一定是自己,而是由父加載器先嘗試加載 3. 父加載器無法加載,再由子類加載 ```mermaid sequenceDiagram Main->>Custom: 加載 class 文件 Custom->>Custom: 檢查緩存 Custom->>System: 沒有緩存則委託加載 System->>System: 檢查緩存 System->>Extension: 沒有緩存則委託加載 Extension->>Extension: 檢查緩存 Extension->>Bootstrap: 沒有緩存則委託加載 Bootstrap->>Extension: 嘗試加載 Extension->>System: 嘗試加載 System->>Custom: 嘗試加載 Custom->>Main: 拋出異常 ``` 上面可以看出來,這是 **類加載的方式是一種經典的 ++遞歸結構++**(加載器、加載器之間是包裝關係,**++不是++ 繼承關係**) :::danger * **所有加載器都無法加載成功的話**? 直到最後如果 Bootstrap 加載器仍無法加載類時,就像呼叫者拋出 `ClassNotFoundException` 異常 ::: :::success * **這種設計與安全性有啥關係**? 透過父加載器先行加載的好處是可以避免使用者(或是惡意程式),載入一個非法的類(或惡意的類),來替換調原先正常的類! ::: ### 加載器特點 * ClassLoader 類加載器有 **兩個重要特點** 1. **命名空間**:有幾個重點概念 * **每個類別載入器都有自己的命名空間**! > 由命名空間由該載入器、所有的父類載入器共同組成 * **在同一個命名空間中不會有相同的類被再次加載** * **如果是由不同命名空間來加載相同類,那該類就可能初始化多次**(被載入多次) > ![](https://hackmd.io/_uploads/rku0_4z62.png) 2. **執行時套件**: * **由同個類別載入器載入的屬於相同套件的類別,組成執行時套件** > ![](https://hackmd.io/_uploads/Sksuc4f62.png) * 決定兩個類是否屬於同一個執行時套件,需要 **比較 ==套件名稱==**、**==類別載入器==**,**兩者要都相同才算屬於同執行套件** > 可以通過兩個 ClassLoader 加載 **同一個 Class 文件,而文件可以被加載 2 次**(因為不同類加載器,內部的緩存會分開) :::info * **這套件區分有什麼效果?** 只有相同套件中的類可以存取(預設存取級別 `Package`)的屬性、函數;這可以限制類的存取 > ![](https://hackmd.io/_uploads/SJaosVf6n.png) ::: ## 類加載測試 * 準備被加載的類:^1.^ `Sample.java`、^2.^ `Apple.java` 1. **`Sample.java` 類** ```java= package classLoader; public class Sample { static { System.out.println("Sample class was loaded by " + Sample.class.getClassLoader()); } public Sample() { System.out.println("Sample class was initialize by " + this.getClass().getClassLoader()); new Apple(); } } ``` 2. **`Apple.java` 類** ```java= package classLoader; public class Apple { static { System.out.println("Apple class was loaded by " + Apple.class.getClassLoader()); } public Apple() { System.out.println("Apple class was initialize by " + this.getClass().getClassLoader()); } } ``` * **自訂類加載器**: 我們來自定義一個類加載器,它的功能是讀取指定的 `.class` 檔,並透過 `ClassLoader#defineClass` 加載進記憶體中 > 以下假設我的 `.class` 檔案會存在 `/Users/Hy-KylePan/IdeaProjects/JavaTest` 下的三個目錄之下(代表了不同 Package) > > 1. `lib_server` 目錄 > 2. `lib_client` 目錄 > 3. `lib_other` 目錄 > > > ![](https://hackmd.io/_uploads/BJ2NVWXa3.png) ```java= public class DefineClassLoader extends ClassLoader { private final String name; private static final String basePath = "/Users/Hy-KylePan/IdeaProjects/JavaTest"; private String subPath; public DefineClassLoader(String name) { super(); this.name = name; } public DefineClassLoader(ClassLoader parent, String name) { super(parent); this.name = name; } public void setSubPath(String subPath) { this.subPath = subPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] data = loadClassAsBytes(name); // Converts an array of bytes into an instance of class `Class` return defineClass(name, data, 0, data.length); } private byte[] loadClassAsBytes(String name) throws ClassNotFoundException { String totalPath = basePath + subPath + "/" + name; String fixPath = totalPath.replace(".", "/"); File file = new File(fixPath + ".class"); try(FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { int index; while ((index = fis.read()) != -1) { baos.write(index); } return baos.toByteArray(); } catch (Exception e) { throw new ClassNotFoundException(); } } @Override public String toString() { return name; } public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { // 加載器 1 DefineClassLoader serverLoader = new DefineClassLoader("serverLoader"); serverLoader.setSubPath("/lib_server"); serverLoader.loadClass("classLoader.Sample").newInstance(); System.out.println(); // 加載器 2 // 設定 parent 加載器為 serverLoader DefineClassLoader clientLoader = new DefineClassLoader(serverLoader, "clientLoader"); clientLoader.setSubPath("/lib_client"); clientLoader.loadClass("classLoader.Sample").newInstance(); System.out.println(); // 加載器 3 // 設定 parent 加載器為 null(其實代表了 BootClassLoader) DefineClassLoader otherLoader = new DefineClassLoader(null, "otherLoader"); otherLoader.setSubPath("/lib_other"); otherLoader.loadClass("classLoader.Sample").newInstance(); System.out.println(); } } ``` 待加載類 與 類加載器 之間的關係如下圖 ``` mermaid graph BT; clientLoader-->|指定,父加載器|serverLoader; serverLoader-->|預設,父加載器|BootClassLoader; otherLoader-->|預設,父加載器|BootClassLoader; 待加載類-.->|加載|serverLoader; 待加載類-.->|加載|clientLoader; 待加載類-.->|加載|otherLoader; ``` ### 驗證 - 命名空間、父委託 * 在創建完自己的類加載器後,我們首先來驗證類加載器 & 命名空間是否有關; 我們使用以下步驟 1. **編譯多個被加載類**:將 `Sample.java`、`Apple.java` 兩個 Java 檔分別編譯到 `lib_server`、`lib_client`、`lib_other` 三個資料夾之下… 讓其都存有對應的 `.class` 檔案 ```shell= ## 各別指令編譯 javac -sourcepath ./src/testClassLoader ./src/classLoader/Apple.java ./src/classLoader/Sample.java -d lib_server javac -sourcepath ./src/testClassLoader ./src/classLoader/Apple.java ./src/classLoader/Sample.java -d lib_client javac -sourcepath ./src/testClassLoader ./src/classLoader/Apple.java ./src/classLoader/Sample.java -d lib_other ``` :::info * 其中的 **`.class` 檔案內容都相同**,只次儲存的位置不同 > 這可以用來驗證相同的 Class 會不會應為加載器的不同,而進行多次加載,或是只進行一次加載 ::: > ![](https://hackmd.io/_uploads/Bkn0rWQTn.png) 2. **編譯自定義的類加載器**:將 `DefineClassLoader.java` 編譯 ```java= javac src/classLoader/DefineClassLoader.java ``` > ![](https://hackmd.io/_uploads/BkrhVf7Th.png) 3. 用 `java` 運行 `DefineClassLoader` 類 ```java= java -classpath ./src classLoader.DefineClassLoader ``` > ![](https://hackmd.io/_uploads/BJffPfXTh.png) * 從結果看來 > ![](https://hackmd.io/_uploads/SJmyoMXTn.png) * **JVM 方法區** * 存在兩組相同的 `Sample`, `Apple` 類,但由於是 **不同類加載進行加載,所有就算相同類,也會載入 2 次** > `serverLoader` & `otherLoader` 會加載類 1. `serverLoader` 加載:由於委託 BootClassLoader 後仍無法加載目標類,所以再退回 `serverLoader` 讓它加載,所以緩存會在 `serverLoader` > BootClass 只加載系統類 ``` mermaid graph BT; serverLoader-->BootClassLoader; BootClassLoader-->serverLoader 待加載類-->serverLoader; ``` 2. `clientLoader` 不會加載,因為 `serverLoader` 已經加載過,所以會使用 `serverLoader` 的緩存 ``` mermaid graph BT; serverLoader-->|第一次|BootClassLoader; clientLoader-->serverLoader serverLoader-->|第二次,已經緩存|serverLoader 待加載類-->clientLoader; ``` 3. `otherLoader` 加載:它的父類加載器為 BootClassLoader(但它無法加載目標類),所以退回到 `otherLoader` 加載; > BootClass 只加載系統類 ``` mermaid graph BT; otherLoader-->BootClassLoader; serverLoader-->BootClassLoader; BootClassLoader-->otherLoader 待加載類-->otherLoader; ``` :::info * 而儘管加載的目標相同(`Sample`, `Apple` 類),但類加載器 `otherLoader`、`serverLoader` 是不同命名空間,所以 **仍會加載兩次** > 這會造成觸發兩次類的初始化 ::: * 由於 **`clientLoader` 加載時受委託機制限制,所以會先去尋找 `severLoader` 類加載器**,而 `severLoader` 已經加載,所以不會再方法區再建構 * **JVM 堆區** * 在讀取類資料方法區時,會同時在堆區建立對應的 Class 對象 :::danger * **這是否代表堆區中有兩個相同的 Class 呢**? **不會**!這是觀察角度問題,**以堆區來看 仍是只有一個 Class**;但如果以 **類加載器來看則是不同 Class** 所以會載入兩次 ::: * 不同類別載入器的命名空間還有以下特點 1. **同一個命名空間中的類別是相互可見的** 2. **子加載器可以看見父類別的命名空間!** **但父載入器的類別,不能看見子類別載入的類別** :::info * 因為子加載器別會有自己的緩存,而在檢查自己的緩存前會先請父加載器檢查它的緩存(反向卻不行); * 所以可以當作子加載器可見父加載器的類別,又由於類的可見,所以可以確定父、子加載器是相同空間 ::: ### 驗證 - 執行套件 * 接著我們來驗證,假設 `serverLoader` 加載器加載成功後,`otherLoader` 是否可以使用它已經加載的類; 我們使用以下步驟 1. 手動移除 `otherLoader` 讀取 `lib_other/classLoader` 資料夾內的內容;移除後 `otherLoader` 就讀取不到指定類 ```shell= rm -rf lib_other/classLoader/* ``` > ![](https://hackmd.io/_uploads/Hkb-6z7T3.png) 2. 用 `java` 運行 `DefineClassLoader` 類 ```java= java -classpath ./src classLoader.DefineClassLoader ``` > ![](https://hackmd.io/_uploads/BJffPfXTh.png) * 從解果看來 就算是內容相同的 `class` 檔案,**由不同 ClassLoader 加載,也不能在 JVM 中共享**;所以會拋出 `ClassNotFound` 異常 > ![](https://hackmd.io/_uploads/ByESaf7an.png) ### 破解命名空間 - 反射 * 我們知道命名空間可以隔離不同類加載器加載的類之間相互訪問,但是我們 **可以使用反射來取得類的實體(取得堆區的 Class 物件)** :::success * **反向搜尋** 由 **堆區** 來找到 **方法區**,所以就可以忽略 ClassLoader 的加載問題!(但前提是類已經被加載進來) ::: ## 類 - 卸載 **回收時機**:當類在堆區的 Class 不再被參考使用,並且 GC 的可達性分析也無法觸及時,Class 物件就會被回收,並結束類的生命週期;並且類結束生命週期後 * 堆區資料被回收 * 方法區資料也被回收 :::info * **至於 ClassLoader 是否會被回收**? 如果是內建(核心)的 ClassLoader 是隨 JVM 生命週期同生共死的(我們自己建立的 ClassLoader 則跟一般類相同) ::: ### 測試 - 類加載器、類卸載 * 由於內建類加載器不會被卸載,所以我們這邊使用上面自定義的類加載器(`DefineClassLoader`),並透過設置、主動 GC 來觸發回收,查看類是否在方法區被卸載並重新創建 ```java= public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, InterruptedException { // 創建第一個類加載 DefineClassLoader serverLoader = new DefineClassLoader("serverLoader"); serverLoader.setSubPath("/lib_server"); Class<?> sClz = serverLoader.loadClass("classLoader.Sample"); Object obj = sClz.newInstance(); // 查看類在方法區的位置 System.out.println("ClassLoader, Sample clz hashcode :" + obj.getClass().hashCode()); // 將原有的強引用設置為 null,讓其離開可達性分析 serverLoader = null; sClz = null; obj = null; // 手動觸發 GC System.gc(); Thread.sleep(1000); System.out.println(); // 創建第二個類加載 serverLoader = new DefineClassLoader("serverLoader"); serverLoader.setSubPath("/lib_server"); sClz = serverLoader.loadClass("classLoader.Sample"); obj = sClz.newInstance(); // 查看類在方法區的位置 System.out.println("ClassLoader recreate, and Sample clz hashcode :" + obj.getClass().hashCode()); } ``` 從結果看來,類確實類重新加載到堆區了!(Class 物件的 hashcode 不同) > ![](https://hackmd.io/_uploads/HJWl5SXp2.png) ## 補充 ### URLClassLoader * 在 JDK 中的 `java.net` 套件中,有提供一個 `URLClassLoader`,它可以從網路上下載一個類,並將其加載進方法區、堆區;以下寫一個使用範例 :::info * 我們可以將一個 class 上傳到雲空間,這裡將上面的 `Apple.class` 傳到 github 空間 ::: ```java= public class UseUrlClassLoader { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException { URL url = new URL("https://github.com/KylePanHy/MyTest/raw/main/Apple.class"); URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{ url }); Class<?> clz = urlClassLoader.loadClass("classLoader.Apple"); Object obj = clz.newInstance(); System.out.println("Class from url, clz name: " + obj.getClass().getName() + ", hashCode: " + obj.hashCode()); } } ``` > ![](https://hackmd.io/_uploads/BkjKjEQT2.png) ## 更多的 Java 語言相關文章 ### Java 語言深入 * 在這個系列中,我們深入探討了 Java 語言的各個方面,從基礎類型到異常處理,從運算子到物件創建與引用細節。點擊連結了解更多! :::info * [**深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節**](https://devtechascendancy.com/basic-types_encoding_reference_variables-scopes/) * [**深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構**](https://devtechascendancy.com/java-compilation_jdk_javadoc_jar_guide/) * [**深入理解 Java 異常處理:從基礎概念到最佳實踐指南**](https://devtechascendancy.com/java-jvm-exception-handling-guide/) * [**深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較**](https://devtechascendancy.com/java-operators-modifiers-key-concepts_equals/) * [**深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用**](https://devtechascendancy.com/java-object-creation_jvm-reference-details/) ::: ### Java IO 相關文章 * 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容! :::warning * [**Java File 操作指南:基礎屬性判斷、資料夾和檔案的創建、以及簡單示範**](https://devtechascendancy.com/java-file-operations-guide/) * [**深入探索 Java 編碼知識**](https://devtechascendancy.com/basic-types_encoding_reference_variables-scopes/) * [**深入理解 Java IO 操作:徹底了解流、讀寫、序列化與技巧**](https://devtechascendancy.com/deep-in-java-io-operations_stream-io/) * [**深入理解 Java NIO:緩衝、通道與編碼 | Buffer、Channel、Charset**](https://devtechascendancy.com/deep-dive-into-java-nio_buf-channel-charset/) ::: ### 深入 Java 物件導向 * 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念! :::danger * [**深入比較介面與抽象類:從多個角度剖析**](https://devtechascendancy.com/comparing-interfaces-abstract-classes/) * [**深度探究物件導向:繼承的利與弊 | Java、Kotlin 為例 | 最佳實踐 | 內部類細節**](https://devtechascendancy.com/deep-dive-into-oop-inheritance/) * [**「類」的生命週期、ClassLoader 加載 | JVM 與 Class | Java 為例**](https://devtechascendancy.com/class-lifecycle_classloader-exploration_jvm/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Java 基礎`