---
# System prepended metadata

title: 類 - 生命週期、ClassLoader
tags: [Java 基礎]

---

---
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 基礎`
