---
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. **類卸載**
> 
:::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);
}
}
```
> 
* **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);
}
}
```
> 
* **父類尚未加載**:如果目標類有父類,並父類也尚未加載:那會先加載父類進方法區並建立堆區的 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);
}
}
```
> 
## 類 - 加載時機
我們前面有說到,類的加載是在我們 **初次 ++主動++ 使用類別時**,而這個初次主動 **主動又有分** 的,**並不是有使用到就算主動**;
主動使用到有分如下行為,這些行為 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
```
> 
### 創建物件
* 建立類的 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 區塊,發現類是否有被加載)
:::
> 
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.");
}
}
```
> 
### 靜態元素
* **靜態元素** 包括如下兩種
* 呼叫 **靜態方法**
```java=
class LoadStaticMethod {
static {
System.out.println("LoadStaticMethod loaded");
}
// 呼叫會觸發加載
static void showMsg() {
System.out.println("HelloWorld");
}
}
```
> 
* 存取類別的 **靜態 ++變數++**
```java=
class LoadStaticField {
// 存取會觸發加載
static int commonValue = 0;
static {
System.out.println("LoadStaticField loaded");
}
}
```
> 
:::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);
}
}
```
> 
我們也可以透過 javac、javap 指令來查看 **常量得值,會被編譯器直接取代,不會呼叫該類**(這是一種編譯器的優化)
> 
:::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();
}
}
```
> 
* 這裡要提早提及一個有關於 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");
}
}
```
> 
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");
}
}
```
> 
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");
}
}
```
> 
* 在 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 不讓其他使用者使用
:::
> 
### 父委託機制
* **父委託機制**(也稱為 雙親委託機制),這是類加載實現的,類加載的核心概念入下:
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. **命名空間**:有幾個重點概念
* **每個類別載入器都有自己的命名空間**!
> 由命名空間由該載入器、所有的父類載入器共同組成
* **在同一個命名空間中不會有相同的類被再次加載**
* **如果是由不同命名空間來加載相同類,那該類就可能初始化多次**(被載入多次)
> 
2. **執行時套件**:
* **由同個類別載入器載入的屬於相同套件的類別,組成執行時套件**
> 
* 決定兩個類是否屬於同一個執行時套件,需要 **比較 ==套件名稱==**、**==類別載入器==**,**兩者要都相同才算屬於同執行套件**
> 可以通過兩個 ClassLoader 加載 **同一個 Class 文件,而文件可以被加載 2 次**(因為不同類加載器,內部的緩存會分開)
:::info
* **這套件區分有什麼效果?**
只有相同套件中的類可以存取(預設存取級別 `Package`)的屬性、函數;這可以限制類的存取
> 
:::
## 類加載測試
* 準備被加載的類:^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` 目錄
>
> > 
```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 會不會應為加載器的不同,而進行多次加載,或是只進行一次加載
:::
> 
2. **編譯自定義的類加載器**:將 `DefineClassLoader.java` 編譯
```java=
javac src/classLoader/DefineClassLoader.java
```
> 
3. 用 `java` 運行 `DefineClassLoader` 類
```java=
java -classpath ./src classLoader.DefineClassLoader
```
> 
* 從結果看來
> 
* **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/*
```
> 
2. 用 `java` 運行 `DefineClassLoader` 類
```java=
java -classpath ./src classLoader.DefineClassLoader
```
> 
* 從解果看來
就算是內容相同的 `class` 檔案,**由不同 ClassLoader 加載,也不能在 JVM 中共享**;所以會拋出 `ClassNotFound` 異常
> 
### 破解命名空間 - 反射
* 我們知道命名空間可以隔離不同類加載器加載的類之間相互訪問,但是我們 **可以使用反射來取得類的實體(取得堆區的 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 不同)
> 
## 補充
### 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());
}
}
```
> 
## 更多的 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 基礎`