Blogger 站內搜尋

2025年2月9日 星期日

Java 7 to 17 HiddenClass

一、前言

一、前言

返回目錄

在 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 的引入,主要是為了滿足以下需求:

  1. 動態代理與框架: 許多框架需要創建匿名的動態代理類別,而 Hidden Class 提供了更輕量、更高效的實現方式。
  2. 動態編譯與腳本: 運行時編譯的程式碼,如 Lambda 表達式或腳本語言的實現,可以使用 Hidden Class 來隔離它們的執行環境。
  3. 程式碼生成: 需要在運行時產生程式碼,但不希望這些程式碼汙染類別命名空間的情況。

三、Hidden Class 的架構與機制

返回目錄

Hidden Class 不是一個獨立的 Java 語言層面的特性,而是 JVM 的內部機制。其主要運作方式如下:

  1. 類別生成: 使用 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 陣列,指定類別的行為。
  2. Internal Representation: Hidden Class 儲存在 JVM 的內部記憶體中,與一般的類別有所不同,它們沒有公開的名稱和類別路徑。
  3. 生命週期管理: Hidden Class 的生命週期會與創建它的 java.lang.invoke.MethodHandles.Lookup 物件相關聯,當 Lookup 物件變得不可到達時, JVM 就可能會回收該 Hidden Class 所佔用的記憶體。Hidden Class 的生命週期也會受到垃圾回收機制 (Garbage Collection, GC) 的影響,當 JVM 執行 GC 時,如果發現沒有程式碼引用 Hidden Class,也會將它回收。
  4. 訪問限制: Hidden Class 只能從創建它們的 MethodHandles.Lookup 執行個體訪問。這限制了它們的存取範圍,確保了它們的隱藏性。

程式碼

MethodHandles.Lookup

Bytecode

Hidden Class Instance

JVM Internal Memory

GC

四、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:

附錄:快速了解什麼是ASM?

5.2 完整的範例程式碼

返回目錄

程式碼解釋:

  1. 引入 ASM 相關類別: 引入 ASM 庫中需要的類別,如 ClassWriter MethodVisitor Opcodes 等。
  2. createHiddenClassBytecode() 方法:
    • 創建 ClassWriter 來生成位元組碼。
    • 使用 visit() 方法指定類別的基本資訊(如版本、修飾符、類別名稱、父類別)。
    • 創建建構子 init() 方法。
    • 創建 hello() 方法。
      • 使用 visitFieldInsn visitMethodInsn 來調用 System.out.println() 印出訊息。
    • 使用 toByteArray() 方法將 ClassWriter 轉為 byte 陣列。
    • 使用 CheckClassAdapter.verify(classReader,false,System.out); 確認產生的 byte code 是符合 JVM 規範的。
  3. 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 測試與驗證

返回目錄

  1. Hidden Class 的名稱: hiddenClass.getName() 會產生類似 HiddenClassImpl/0x0000029954150000 的名稱,表示這是由 MethodHandles.Lookup 動態產生的,而不是一個實際存在的類別名稱。
  2. hiddenClass.isHidden() : 這個方法會返回 true ,表示這個類別是一個 Hidden Class。
  3. Class.forName() 失敗: 如果你嘗試使用 Class.forName(hiddenClass.getName()) 獲取 Hidden Class,會拋出 ClassNotFoundException ,因為它不是一個公開的、可以被類別載入器找到的類別。
  4. ClassLoader 失敗: 使用 classLoader.loadClass(hiddenClass.getName()) 也會拋出 ClassNotFoundException ,因為這個類別並沒有被註冊到 ClassLoader 中。
  5. HashCode 的差異: 相同的程式碼產生的 byte[] 產生的 hiddenClass hashCode 都會不相同,代表每次都會產生不同的 class。
  6. 內部方法存取: 我們可以透過 getMethods() 取得其類別中定義的方法,但是沒辦法透過類別名稱存取到該類別。

5.4 如何發現 Hidden Class 的存在(間接方式)

返回目錄

由於 Hidden Class 本身是隱藏的,無法直接透過類別載入器找到。但你可以透過以下方式間接發現它的存在:

  1. 觀察堆疊追蹤: 如果程式碼中調用了 Hidden Class 的方法,並且拋出異常,堆疊追蹤中可能會出現類似 HiddenClassImpl/0x0000029954150000 的類別名稱。
  2. 監測記憶體: 透過 JVM 的記憶體監控工具,你可以觀察到 Hidden Class 的實例在記憶體中的存在。這些實例通常不會被歸類為普通的類別。
  3. JOL 工具: Java Object Layout (JOL) 是一個可以深入觀察 JVM 物件結構的工具。你可以使用它來檢視 Hidden Class 的內部結構,並確認它是一個 Hidden Class。
  4. 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 為例):
  • UI 介面查看資訊:
    • 在 MAT 中,選擇 "File" -> "Open Heap Dump",然後選擇你的 .hprof 檔案。

      Open Heap Dump
      Select dump file
      Getting Started Wizard

    • 總覽 (Overview):MAT 提供概觀視圖,顯示 Heap Dump 的總體資訊,例如物件數量、總記憶體使用量等。

      Overview

    • Histogram: 顯示每個類別的物件數量、佔用大小等資訊。你可以透過正規表達式或是關鍵字來搜尋類別。

      • Histogram 視圖,可以讓你快速掌握物件佔用記憶體的情況。
      • 可以按類別名稱、物件數量或佔用大小排序。
      • 適合找出佔用最多記憶體的類別。
        • 使用關鍵字來搜尋 Hidden Class:

      Histogram
      Histogram Search Regex
      Histogram Search Keyword

    • Dominator Tree: 顯示記憶體中的物件支配關係,找出佔用最多記憶體的物件。 * Dominator Tree 視圖,可以顯示物件的支配關係,找出佔用最多記憶體的物件。

      • 每個節點代表一個物件,它的子節點是被它支配的物件。
      • 適合找出記憶體洩漏的根源。
        • 使用正規表達式或是關鍵字來搜尋 Hidden Class:

      Dominator Tree
      Dominator Tree Regex
      Dominator Tree keyword

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 程式碼有很大的不同。

  1. 了解 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: 類別的其他屬性。

  2. 理解 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 : 從方法返回。
      • ... 等等
  3. 閱讀 ASM 程式碼:

    • 類別定義:
      • 使用 ClassWriter 來建立類別,並使用 visit() 方法來設定類別的基本資訊。
    • 成員變數定義:
      • 使用 FieldVisitor 來定義成員變數,並使用 visitField() 方法來設定成員變數的資訊,例如修飾符、名稱和型態。
    • 方法定義:
      • 使用 MethodVisitor 來定義方法,並使用 visitMethod() 方法來設定方法的資訊,例如修飾符、名稱、參數類型和返回類型。
    • 方法程式碼:
      • 使用 MethodVisitor visitCode() 方法開始訪問方法的程式碼。
      • 使用 visitVarInsn() visitFieldInsn() visitMethodInsn() 等方法來生成 JVM 指令。
      • 使用 visitInsn() 方法來插入基本的 JVM 指令,例如 RETURN
      • 使用 visitMaxs() 方法來設定堆疊和區域變數的最大大小。
      • 使用 visitEnd() 方法來結束訪問方法。
  4. 參考 ASM 的文件和範例:

    • ASM 官方文件: https://asm.ow2.io/
    • ASM 範例: 在網路搜尋 "ASM examples" 可以找到許多 ASM 的範例程式碼。
    • 相關書籍: 你可以閱讀一些介紹 Java 位元組碼操作和 ASM 的書籍。
  5. 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 : 類別檔案中的常數池。
  6. ASM 的程式碼風格與一般的 Java 程式碼不同,它主要關注於:

    • 位元組碼層級的操作: ASM 直接操作 JVM 指令,因此程式碼的抽象層級比較低。
    • Visitor Pattern: ASM 使用 Visitor 模式來處理類別檔案,因此程式碼的結構比較固定。
    • 操作碼的使用: ASM 程式碼中大量使用 Opcodes 常數,這需要你了解 JVM 指令的含義。

返回目錄

沒有留言:

張貼留言