一、前言
在 Java 的發展歷程中,不斷有新的特性被引入,旨在提升開發效率、程式效能以及更靈活的應用場景。「Hidden Class」 隱藏類別,作為 Java 15 中引入的一個關鍵特性,並在 Java 17 中得到加強,是 Java 虛擬機器 (JVM) 底層的一項重大改進。它為動態生成類提供了更細粒度的控制,允許開發者建立不會被發現的類別,並在 JVM 內部運作。本篇文章將深入探討 Java 7 至 17 中有關「Hidden Class」的發展歷程與相關應用,並著重分析其特性、架構、工具與提供範例。
目錄
二、Hidden Class 的概念與起源
在 Java 7 之前,動態產生類別主要依賴於類別載入器 (ClassLoader) 和位元組碼操作框架 (如 ASM 或 Javassist)。然而,這些方法產生類別後,這些類別會成為 JVM 的公開一部分,具有固定的生命週期和類別名稱。Hidden Class 的出現打破了這種限制。它允許在運行時創建不公開的類別,這些類別:
- 不會被其他類別或類別載入器發現: 它們存在於 JVM 的內部,不暴露給外部。
- 具有臨時性: 它們的生命週期與創建它們的程式碼的生命週期相綁定。
- 可以被回收: 當沒有程式碼引用它們時,GC 會回收它們的記憶體。
Hidden Class 的引入,主要是為了滿足以下需求:
- 動態代理與框架: 許多框架需要創建匿名的動態代理類別,而 Hidden Class 提供了更輕量、更高效的實現方式。
- 動態編譯與腳本: 運行時編譯的程式碼,如 Lambda 表達式或腳本語言的實現,可以使用 Hidden Class 來隔離它們的執行環境。
- 程式碼生成: 需要在運行時產生程式碼,但不希望這些程式碼汙染類別命名空間的情況。
三、Hidden Class 的架構與機制
Hidden Class 不是一個獨立的 Java 語言層面的特性,而是 JVM 的內部機制。其主要運作方式如下:
-
類別生成:
使用
java.lang.invoke.MethodHandles.Lookup
來創建 Hidden Class 的實例。MethodHandles.Lookup
物件是建立 Hidden Class 的核心,它不僅包含建立 Hidden Class 的能力,也決定了該 Hidden Class 的訪問權限。你可以使用MethodHandles.lookup()
取得一般的 Lookup 物件,或是使用MethodHandles.privateLookupIn()
來取得有權限可以存取 private 屬性的 Lookup 物件。通過提供位元組碼 (byte code) 或一個 byte 陣列,指定類別的行為。 - Internal Representation: Hidden Class 儲存在 JVM 的內部記憶體中,與一般的類別有所不同,它們沒有公開的名稱和類別路徑。
-
生命週期管理:
Hidden Class 的生命週期會與創建它的
java.lang.invoke.MethodHandles.Lookup
物件相關聯,當Lookup
物件變得不可到達時, JVM 就可能會回收該 Hidden Class 所佔用的記憶體。Hidden Class 的生命週期也會受到垃圾回收機制 (Garbage Collection, GC) 的影響,當 JVM 執行 GC 時,如果發現沒有程式碼引用 Hidden Class,也會將它回收。 -
訪問限制:
Hidden Class 只能從創建它們的
MethodHandles.Lookup
執行個體訪問。這限制了它們的存取範圍,確保了它們的隱藏性。
graph LR A[程式碼] --> B(MethodHandles.Lookup); B --> C{Bytecode}; C --> D[Hidden Class Instance]; D --> E[JVM Internal Memory]; F[GC] --> E; style A fill:#f9f,stroke:#333,stroke-width:2px,color:#000000 style D fill:#ccf,stroke:#333,stroke-width:2px,color:#000000
四、Java 版本演進與 Hidden Class
-
Java 7 - 14:
此階段不直接支援 Hidden Class。但 JVM 本身提供了相關的 API,可以透過
sun.misc.Unsafe
來間接操作。這種方式非常複雜且不安全,不建議使用。 -
Java 15:
Hidden Class 以 JEP 371 的形式被正式引入。
java.lang.invoke.MethodHandles.Lookup
中新增了defineHiddenClass
方法,提供了一個標準化的方式來建立 Hidden Class。 -
Java 17:
JEP 416 增強了 Hidden Class,加入了更完整的 API,並提高了實用性,包含:
-
允許使用
MethodHandles.Lookup::defineHiddenClassWithDirectSuperclass
定義一個有明確父類別的隱藏類別。 -
Class::isHidden
方法可以檢查類別是否為 Hidden Class。 -
Class::getNestHost
方法用於取得 Hidden Class 所屬的巢狀類別宿主。 -
除了
NESTMATE
選項,也可以使用STRONG
選項,來指定隱藏類別與定義類別載入器之間具有強關聯。如果使用STRONG
選項,則它的卸載行為與普通類別相同, 只有當其定義類別載入器不再可到達時,它才能被垃圾回收器回收。
-
允許使用
五、工具與範例
以下範例將更詳細地示範如何使用 ASM 庫來動態生成 Hidden Class 的位元組碼,並提供測試方法來驗證其隱藏性。
5.1 引入 ASM 庫
首先,你需要在你的專案中引入 ASM 庫。如果你使用 Maven,可以在
pom.xml
中添加以下依賴:
如果你使用 Gradle,可以在
build.gradle
中添加以下依賴:
或是直接下載 ASM 第三方套件,加入至 classpath:
- https://repo1.maven.org/maven2/org/ow2/asm/asm/9.5/asm-9.5.jar
- https://repo1.maven.org/maven2/org/ow2/asm/asm-util/9.5/asm-util-9.5.jar
- https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.5/asm-tree-9.5.jar
- https://repo1.maven.org/maven2/org/ow2/asm/asm-analysis/9.5/asm-analysis-9.5.jar
5.2 完整的範例程式碼
程式碼解釋:
-
引入 ASM 相關類別:
引入 ASM 庫中需要的類別,如
ClassWriter
、MethodVisitor
、Opcodes
等。 -
createHiddenClassBytecode()
方法:-
創建
ClassWriter
來生成位元組碼。 -
使用
visit()
方法指定類別的基本資訊(如版本、修飾符、類別名稱、父類別)。 -
創建建構子
init()
方法。 -
創建
hello()
方法。-
使用
visitFieldInsn
和visitMethodInsn
來調用System.out.println()
印出訊息。
-
使用
-
使用
toByteArray()
方法將ClassWriter
轉為 byte 陣列。 -
使用
CheckClassAdapter.verify(classReader,false,System.out);
確認產生的 byte code 是符合 JVM 規範的。
-
創建
-
main()
方法:-
使用
createHiddenClassBytecode()
產生 Hidden Class 的 byte array。 -
使用
MethodHandles.lookup()
取得MethodHandles.Lookup
物件。 -
使用
lookup.defineHiddenClass()
來定義 Hidden Class。 -
實例化 Hidden Class 並調用
hello()
方法。 - 印出 Hidden Class 的名稱、是否為隱藏類別。
-
嘗試使用
Class.forName()
及 ClassLoader 來存取 Hidden class。
-
使用
輸出結果:
5.3 測試與驗證
-
Hidden Class 的名稱:
hiddenClass.getName()
會產生類似HiddenClassImpl/0x0000029954150000
的名稱,表示這是由MethodHandles.Lookup
動態產生的,而不是一個實際存在的類別名稱。 -
hiddenClass.isHidden()
: 這個方法會返回true
,表示這個類別是一個 Hidden Class。 -
Class.forName()
失敗: 如果你嘗試使用Class.forName(hiddenClass.getName())
獲取 Hidden Class,會拋出ClassNotFoundException
,因為它不是一個公開的、可以被類別載入器找到的類別。 -
ClassLoader 失敗:
使用
classLoader.loadClass(hiddenClass.getName())
也會拋出ClassNotFoundException
,因為這個類別並沒有被註冊到 ClassLoader 中。 -
HashCode 的差異:
相同的程式碼產生的
byte[]
產生的hiddenClass
其hashCode
都會不相同,代表每次都會產生不同的 class。 -
內部方法存取:
我們可以透過
getMethods()
取得其類別中定義的方法,但是沒辦法透過類別名稱存取到該類別。
5.4 如何發現 Hidden Class 的存在(間接方式)
由於 Hidden Class 本身是隱藏的,無法直接透過類別載入器找到。但你可以透過以下方式間接發現它的存在:
-
觀察堆疊追蹤:
如果程式碼中調用了 Hidden Class 的方法,並且拋出異常,堆疊追蹤中可能會出現類似
HiddenClassImpl/0x0000029954150000
的類別名稱。 - 監測記憶體: 透過 JVM 的記憶體監控工具,你可以觀察到 Hidden Class 的實例在記憶體中的存在。這些實例通常不會被歸類為普通的類別。
- JOL 工具: Java Object Layout (JOL) 是一個可以深入觀察 JVM 物件結構的工具。你可以使用它來檢視 Hidden Class 的內部結構,並確認它是一個 Hidden Class。
- MAT 工具: Memory Analyzer Tool (MAT) 這是一個強大的 Java Heap Dump 分析工具,由 Eclipse 基金會開發。MAT 可以幫助你分析 Java 應用程式的記憶體使用情況,找出記憶體洩漏、資源耗盡等問題,並提供深入的分析和診斷功能。
5.5 使用 MAT 工具查看 Heap Dump 檔案
▲ 查閱PID
程式在執行中時,查閱 pid,後續用來產生 Heap Dump 檔案。 你可以使用
jps
或
jcmd
來取得 JVM 程序的 PID。
-
jps
指令: -
jcmd
指令:
▲ memory dump
取得 Heap Dump 檔案:你可以使用
jmap
或
jcmd
命令來產生 heap dump 檔案。
-
使用
jmap
指令: -
使用
jcmd
指令:
▲ 使用MAT
- 下載 MAT: 你可以從 Eclipse 官網下載 MAT: https://eclipse.dev/mat/download/ ,這篇文章使用 1.16.1 版本。
- 解壓縮: 解壓縮下載的壓縮檔到你的電腦上。
-
執行 MAT: 執行解壓縮資料夾中的
MemoryAnalyzer.exe
(Windows) 或MemoryAnalyzer
(Linux/macOS)。-
執行時,指定 Java 路徑(以 Windows cmd 為例):
-
執行時,指定 Java 路徑(以 Windows cmd 為例):
-
UI 介面查看資訊:
-
在 MAT 中,選擇 "File" -> "Open Heap Dump",然後選擇你的
.hprof
檔案。 -
總覽 (Overview):MAT 提供概觀視圖,顯示 Heap Dump 的總體資訊,例如物件數量、總記憶體使用量等。
-
Histogram: 顯示每個類別的物件數量、佔用大小等資訊。你可以透過正規表達式或是關鍵字來搜尋類別。
- Histogram 視圖,可以讓你快速掌握物件佔用記憶體的情況。
- 可以按類別名稱、物件數量或佔用大小排序。
-
適合找出佔用最多記憶體的類別。
- 使用關鍵字來搜尋 Hidden Class:
-
Dominator Tree: 顯示記憶體中的物件支配關係,找出佔用最多記憶體的物件。 * Dominator Tree 視圖,可以顯示物件的支配關係,找出佔用最多記憶體的物件。
- 每個節點代表一個物件,它的子節點是被它支配的物件。
-
適合找出記憶體洩漏的根源。
- 使用正規表達式或是關鍵字來搜尋 Hidden Class:
-
5.6 Hidden Class 的限制
Hidden Class 雖然提供了動態產生類別的強大功能,但也存在一些限制:
- 無法序列化: Hidden Class 不能被序列化,這表示你無法將它們儲存到檔案或通過網路傳輸。
-
反射限制:
你無法使用反射 API (如
Class.forName()
) 來獲取 Hidden Class 的java.lang.Class
物件。 雖然可以透過getMethods
來取得它的方法,但是無法使用getMethod(String name)
來透過方法名稱存取。 -
類別載入器隔離:
Hidden Class 無法通過一般的類別載入器被載入,它們只能從創建它們的
MethodHandles.Lookup
物件訪問。 - 不適用於所有框架: 某些框架可能不支援 Hidden Class,例如,一些需要序列化或反射能力的框架可能無法使用 Hidden Class。
- Debugging 困難: 因為 Hidden Class 是隱藏的,所以偵錯它們可能比較困難。
- 性能考量: 動態生成位元組碼會產生一定的效能開銷,需要謹慎使用。
5.7 範例結論
這個擴充的範例展示了如何使用 ASM 庫動態生成 Hidden Class 的位元組碼,並透過各種方法驗證了 Hidden Class 的隱藏特性。同時也展示了如何間接發現它們的存在。透過這個範例,你應該能更全面地理解 Hidden Class 的運作機制和使用場景。
請注意,上述程式碼範例是基於 Java 17 的,如果你使用較舊的 Java 版本,可能需要進行調整。
六、總結
「Hidden Class」 作為 Java 15 中引入,並在 Java 17 中得到加強的一項關鍵特性,為動態產生類別提供了更精細的控制和靈活性。它不僅在框架、動態編譯和程式碼生成等領域有著廣泛的應用前景,也為 Java 開發者提供了更強大的工具來實現更複雜的應用場景。透過本文的分析,開發者可以更好地理解 Hidden Class 的機制、架構以及實際應用,並在適當的場景中使用它來提升開發效率和程式效能。Hidden Class 的引入,也標誌著 Java 平台在動態語言支援和底層優化方面邁出了重要的一步。
附錄
快速了解什麼是ASM?
你看到使用 ASM 庫的程式碼時,會覺得程式碼比較奇怪和難以理解。ASM 是一個底層的位元組碼操作框架,它直接處理 Java 類別檔案的結構,因此它的程式碼風格和一般的 Java 程式碼有很大的不同。
-
了解 Java 類別檔案結構:
-
基礎: 在深入學習 ASM 之前,你需要了解 Java 類別檔案 (Class File) 的基本結構,你可以使用
javap -verbose <ClassName>
來查看 Java 類別檔案的詳細資訊。javap
指令是 Java 自帶的反編譯工具,可以讓你檢視編譯後的 class 檔案內容。-
例如以下是
java.lang.Object
的javap -verbose
輸出結果:
-
例如以下是
-
Magic Number: 用於識別類別檔案的標頭。
-
Version: 類別檔案的版本,包含 minor version 和 major version。
-
Constant Pool: 常數池,包含字串、類別名稱、方法名稱等常數。
-
Access Flags: 類別的存取修飾符,例如 public、abstract 等。
-
This Class: 類別自身的名稱。
-
Super Class: 父類別的名稱。
-
Interfaces: 實作的介面列表。
-
Fields: 類別的成員變數。
-
Methods: 類別的方法。
-
Attributes: 類別的其他屬性。
-
-
理解 ASM 的核心概念:
-
Visitor Pattern:
ASM 使用 Visitor 模式來處理類別檔案的結構。
-
ClassVisitor
: 用於訪問類別的資訊。 -
FieldVisitor
: 用於訪問成員變數的資訊。 -
MethodVisitor
: 用於訪問方法的資訊。 -
AnnotationVisitor
: 用於訪問注解的資訊。
-
-
ClassWriter
: 用於生成位元組碼。 -
ClassReader
: 用於讀取現有的類別檔案。 -
Opcodes
: ASM 提供的操作碼 (Opcode) 常數,用於表示 JVM 指令。-
Opcodes.INVOKESPECIAL
: 呼叫建構式或是父類別的方法 -
Opcodes.ALOAD
: 將區域變數載入堆疊。 -
Opcodes.GETSTATIC
: 取得靜態欄位。 -
Opcodes.LDC
: 將常數載入堆疊。 -
Opcodes.INVOKEVIRTUAL
: 呼叫虛擬方法。 -
Opcodes.RETURN
: 從方法返回。 - ... 等等
-
-
Visitor Pattern:
ASM 使用 Visitor 模式來處理類別檔案的結構。
-
閱讀 ASM 程式碼:
-
類別定義:
-
使用
ClassWriter
來建立類別,並使用visit()
方法來設定類別的基本資訊。
-
使用
-
成員變數定義:
-
使用
FieldVisitor
來定義成員變數,並使用visitField()
方法來設定成員變數的資訊,例如修飾符、名稱和型態。
-
使用
-
方法定義:
-
使用
MethodVisitor
來定義方法,並使用visitMethod()
方法來設定方法的資訊,例如修飾符、名稱、參數類型和返回類型。
-
使用
-
方法程式碼:
-
使用
MethodVisitor
的visitCode()
方法開始訪問方法的程式碼。 -
使用
visitVarInsn()
、visitFieldInsn()
、visitMethodInsn()
等方法來生成 JVM 指令。 -
使用
visitInsn()
方法來插入基本的 JVM 指令,例如RETURN
。 -
使用
visitMaxs()
方法來設定堆疊和區域變數的最大大小。 -
使用
visitEnd()
方法來結束訪問方法。
-
使用
-
類別定義:
-
參考 ASM 的文件和範例:
- ASM 官方文件: https://asm.ow2.io/
- ASM 範例: 在網路搜尋 "ASM examples" 可以找到許多 ASM 的範例程式碼。
- 相關書籍: 你可以閱讀一些介紹 Java 位元組碼操作和 ASM 的書籍。
-
ASM 的核心概念總結:
-
Visitor Pattern:
ASM 使用 Visitor 模式,透過
ClassVisitor
,MethodVisitor
,FieldVisitor
等類別來走訪和修改類別檔案。 -
ClassWriter
: 用於產生類別的位元組碼。 -
Opcodes
: ASM 提供操作碼,例如Opcodes.ALOAD
,Opcodes.GETSTATIC
,Opcodes.INVOKEVIRTUAL
,Opcodes.RETURN
等,來產生 JVM 指令。 -
MethodVisitor
: 用來產生 method 的 byte code。 -
FieldVisitor
: 用來產生欄位的 byte code。 -
Constant Pool
: 類別檔案中的常數池。
-
Visitor Pattern:
ASM 使用 Visitor 模式,透過
-
ASM 的程式碼風格與一般的 Java 程式碼不同,它主要關注於:
- 位元組碼層級的操作: ASM 直接操作 JVM 指令,因此程式碼的抽象層級比較低。
- Visitor Pattern: ASM 使用 Visitor 模式來處理類別檔案,因此程式碼的結構比較固定。
-
操作碼的使用: ASM 程式碼中大量使用
Opcodes
常數,這需要你了解 JVM 指令的含義。
沒有留言:
張貼留言