類載入機制與反射

Bacer發表於2021-09-09

類載入機制與反射

Java 類載入器除了根類載入器外,其他類載入器都是使用 Java 語言編寫的,所以程式設計師完全可以開發自己的類載入器,通過使用自定義類載入器,可以完成一些特定的功能。

本章重點介紹 java.lang.reflect 包下的介面和類,包括 Class、Method、Field、Constructor 和 Array 等,分別代表類、方法、成員變數、構造器和陣列,Java 程式可以使用這些類動態地獲取某個物件、某個類的執行時資訊,並可以動態地建立 Java 物件,動態呼叫 Java 方法,訪問並修改指定物件的成員變數值。此外,該包還含有Type 和 ParameterizedType 兩個介面,其中 Type 是 Class 類所實現的介面,而 ParameterizedType 則代表一個帶泛型引數的型別。

使用 Proxy 和 InvocationHandler 來建立 JDK 動態代理,並通過 JDK 動態代理介紹高層次解耦的方法,並講解 JDK 動態代理和 AOP(Aspect Orient Programming,面向切面程式設計)之間的內在關係。

類的載入、連線和初始化

系統可能在第一次使用某個類時載入該類,也可能採用預載入機制來載入某個類。(類載入的途徑)

JVM 和類

當呼叫 java 命令執行某個 Java 程式時,該命令將會啟動一個 Java 虛擬機器程式,不管該 Java 程式有多麼複雜,該程式啟動了多少個執行緒,他們都處於該 Java 虛擬機器程式裡。正如多執行緒中所說,同一個 JVM 的所有執行緒、所有變數都處於同一個程式裡,它們都使用該 JVM 程式的記憶體區。

當系統出現以下幾種情況時,JVM 程式將被終止:

  • 程式執行到最後正常結束
  • 程式執行到使用 System.exit() 或 Runtime.getRuntime().exit() 程式碼處結束程式
  • 程式執行過程中遇到未捕獲的異常或錯誤而結束
  • 程式所在平臺強制結束了 JVM 程式

至此可以看出,當 Java 程式執行結束時,JVM 程式結束,該程式在記憶體中的狀態將會丟失。

(兩次執行 Java程式處於兩個不同的 JVM 程式中,兩個 JVM 之間並不會共享資料。)

類的載入

當程式主動使用某個類時,如果該類還未被載入到記憶體中,則系統會通過載入、連線、初始化三個步驟來對該類進行初始化。如果沒有意外,JVM 將會連續完成這三個步驟,所以有時也把這三個步驟統稱為類載入或類初始化。

類載入指的是將類的 class 檔案讀入記憶體,併為之建立一個 java.lang.Class 物件,也就是說,當程式中使用任何類時,系統都會為止建立一個 java.lang.Class 物件。

正如物件導向中所說:類是某一類物件的抽象,類是概念層次的東西。

但有沒有想過:類也是一種物件。就像平常說概念主要用於定義、描述其他事物,但概念本身也是一種事物,那麼概念本身也需要被描述。但是是就是這樣,每個類是一批具有相同特徵的物件的抽象,而系統中所有的類實際上也是例項,它們都是 java.lang.Class 的例項。

類的載入由類載入器完成,類載入器通常由 JVM 提供,這些類載入器也是前面所有程式執行的基礎,JVM 提供的這些類載入器通常被稱為系統載入器。除此之外,開發者可以通過繼承 ClassLoader 基類來建立自己的類載入器。

通過使用不同的類載入器,可以從不同來源載入類的二進位制資料,通常有如下幾種來源:

  • 從本地檔案系統載入 class 檔案,這時絕大部分示例程式的載入方式
  • 從 JAR 包載入 class 檔案,這種方式也是很常見的,JDBC 程式設計時用到的資料庫驅動類就是放在 JAR 檔案中,JVM 可以從 JAR 包中直接載入該 class 檔案
  • 通過網路載入 class 檔案
  • 把一個 Java 原始檔動態編譯,並執行載入

類載入器通常無須等到“首次使用”該類時才載入該類,Java 虛擬機器規範允許系統預先載入某些類。

類的連線

當類被載入後,系統為之生成一個對應的 Class 物件,接著將會進入連線階段,連線階段負責把類的二進位制資料合併到 JRE 中。連線階段又可細分為如下三個階段:

  1. 驗證:驗證階段用於檢驗被載入的類是否有正確的內部結構,並和其他類協調一致
  2. 準備:類準備階段則負責為類的類變數分配記憶體,並設定預設初始值(這個階段並不會執行初始化程式碼)
  3. 解析:將類的二進位制資料中的符號引用替換成直接引用

類的初始化

在類的初始化階段,虛擬機器負責對類進行初始化,主要就是對類變數進行初始化。在 Java 類中對類變數指定初始值有兩種方式:1. 宣告類變數時指定初始值;2. 使用靜態初始化塊為類變數指定初始值。

宣告變數時指定初始值,靜態初始化塊都將被當成類的初始化語句,JVM 會按這些語句在程式中的排列順序一次執行它們。

JVM 初始化一個類包含如下幾個步驟:

  1. 假如這個類還沒有被載入和連線,則程式載入並連線該類
  2. 假如該類的直接父類還沒有被初始化,則先初始化其直接父類(到步驟1開始執行)
  3. 假如類中有初始化語句,則系統依次執行這些初始化語句

經過上面的步驟,JVM 最先初始化的總是 java.lang.Object 類。當程式主動使用任何一個類時,系統會保證該類以及所有父類(包括直接父類和間接父類)都會被初始化。

類初始化的時機

當 Java 程式首次通過下面 6 種方式來使用某個類或介面時,系統就會初始化該類或介面:

  • 建立類的例項。為某個類建立例項的方式包括:使用 new 操作符來建立例項,通過反射來建立例項,通過反序列化的方式來建立例項。
  • 呼叫某個類的類方法
  • 訪問某個類或介面的類變數,或為該類變數賦值
  • 使用反射方式來強制建立某個類或介面對應的 java.lang.Class 物件
  • 初始化某個類的子類。當初始化某個類的子類時,該子類的所有父類都會被初始化
  • 直接使用 java.exe 命令來執行某個主類

對於一個 final 型的類變數,如果該類變數的值在編譯時就可以確定下來,那麼這個類變數相當於“巨集變數”。Java 編譯器會在編譯時直接把這個類變數出現的地方替換成它的值,因此即使程式使用該靜態類變數,也不會導致該類的初始化。反之,如果 final 修飾的類變數的值不能在編譯時確定下來,則必須等到執行時才可以確定該類變數的值,如果通過該類來訪問它的類變數,則會導致該類變數被初始化。(見 Demo21.java

還有一點,當使用 ClassLoader 類的 loadClass() 方法來載入某個類時,該方法只是載入該類,並不會執行該類的初始化。但是用 Class 的 forName() 靜態方法會導致強制初始化該類。

類載入器

類載入器負責將 .class 檔案載入到記憶體中,併為之生成對應的 java.lang.Class 物件。

類載入器簡介

類載入器負責載入所有的類,系統為所有被載入記憶體中的類生成一個 java.lang.Class 例項。一旦一個類被載入 JVM 中,同一個類就不會被再次載入了。

那麼,什麼才算“同一個類”?正如一個物件有一個唯一的標識一樣,一個載入 JVM 的類也有一個唯一的標識。在 Java 中,一個類用其全限定類名(包括報名和類名)作為標識;但在 JVM 中,一個類用其全限定類名和其類載入器作為唯一標識。

當 JVM 啟動時,會形成由三個類載入器組成的初始類載入器層次結構:

  • Bootstrap ClassLoader,根類載入器
  • Extension ClassLoader,擴充套件類載入器
  • System ClassLoader,系統類載入器

Bootstrap ClassLoader 被成為引導(也成為原始或根)類載入器,它負責載入 Java 的核心類。在 Sun 的 JVM 中,當執行 java.exe 命令時,使用 -Xbootclasspath 選項或使用 -D 選項指定 sun.boot.class.path 系統屬性值可以指定載入附加的類。

根類載入器非常特殊,它並不是 java.lang.ClassLoader 的子類,而是由 JVM 自身實現的。

Extension ClassLoader(擴充套件類載入器)負責載入 JRE 的擴充套件目錄(%JAVA_HOME%/jre/lib/ext 或者由 java.ext.dirs 系統屬性指定的目錄)中 JAR 包的類。通過這種方式就可以為 Java 擴充套件核心類以外的新功能,只要把自己開發的類打包成 JAR 檔案,然後放入 %JAVA_HOME%/jre/lib/ext 路徑即可。

System ClassLoader((系統)類載入器)負責在 JVM 啟動時載入來自 java 命令的 -classpath 選項、java.class.path 系統屬性,或 CLASSPATH 環境變數所指定的 JAR 包和路徑。

程式可以通過 ClassLoader 的靜態方法 getSystemClassLoader() 來獲取系統類載入器。如果沒有特別指定,則使用者定義的類載入器都以類載入器作為父載入器。

類載入機制

JVM 的類載入機制主要有如下三種:

  • 全盤負責。所謂全盤負責,就是當一個類載入器負責載入某個 Class 時,該 Class 所依賴的和引用的其他 Class 也將由該類載入器負責載入,除非顯式使用另外一個類載入器來載入
  • 父類委託。所謂父類委託,則時先讓 parent(父)類載入器試圖載入該 Class,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類
  • 快取機制。快取機制將會保證所有載入過的 Class 都會被快取,當程式中需要使用某個 Class 時,類載入器先從快取區中搜尋該 Class,只有當快取區中不存在該 Class 物件時,系統才會讀取該類對應的二進位制資料,並將其轉換成 Class 物件,存入快取區中。這就是為什麼修改了 Class 後,必須重新啟動 JVM,程式所作的修改才會生效的原因

在這裡插入圖片描述

類載入器之間的父子關係(類載入器例項之間的關係)並不是類繼承上的父子關係。

除了可以使用 Java 提供的類載入器之外,開發者也可以實現自己的類載入器,自定義的類載入器通過繼承 ClassLoader 來實現。

系統載入器是 AppClassLoader 的例項,擴充套件載入器是 ExtClassLoader 的例項。實際上,這兩個類都是 URLClassLoader 的例項。

類載入器載入 Class 大致要經過如下 8 個步驟:

在這裡插入圖片描述

其中,第 5、6 布允許重寫 ClassLoader 的 findClass() 方法來實現自己的載入策略,甚至重寫 loadClass() 方法來實現自己的載入過程。

建立並使用自定義的類載入器

JVM 中除根類載入器之外的所有類載入器都是 ClassLoader 子類的例項,開發者可以通過擴充套件 ClassLoader 的子類,並重寫該 ClassLoader 所包含的方法來實現自定義的類載入器。ClassLoader 中包含了大量的 protected 方法(這些方法都可以被子類重寫)。

ClassLoader 類有如下兩個關鍵方法:

// 用指定的名稱載入類
Class<?> loadClass(String name)
// 該方法是 ClassLoader 的入口點,根據指定名稱來載入類,系統就是呼叫 ClassLoader 的該方法來獲取指定類對應的 Class 物件
protected Class<?> loadClass(String name, boolean resolve)

如果需要實現自定義的 ClassLoader,則可以通過重寫以上兩個方法來實現,通常推薦重寫 findClass() 方法,而不是重寫 loadClass() 方法。loadClass() 方法的執行步驟如下:

  1. 用 findLoadedClass(String) 來檢查是否已經載入類,如果已經載入則直接返回(緩衝機制)
  2. 在父類載入器上呼叫 loadClass() 方法。如果父類載入器為 null,則使用根類載入器來載入
  3. 用 findClass(String) 方法查詢類

從上面可以看出,重寫 findClass() 方法可以避免覆蓋預設類載入器的父類委託、緩衝機制兩種策略,如果重寫 loadClass() 方法,則實現邏輯更為複雜。

在 ClassLoader 裡還有一個核心方法:Class defineClass(String name, byte[] b, int off, int len),該方法負責將指定類的位元組碼檔案(即 Class 檔案)讀入位元組陣列 byte[] b 內,並把它轉換為 Class 物件,該位元組碼檔案可以來源於檔案、網路等。defineClass() 方法管理 JVM 的許多複雜的實現,它負責將位元組碼分析成執行時資料結構,並校驗有效性等。不過該方法是 final 的,不可以重寫。

此外,ClassLoader 還包含如下一些普通方法:

// 從本地檔案系統裝入檔案。它在本地檔案系統中尋找類檔案,如果存在,就使用 defineClass() 方法將原始位元組轉換成 Class 物件,以將該檔案轉換成類
protected Class<?> findSystemClass(String name)
// 返回系統類載入器
static ClassLoader getSystemClassLoader()
// 獲取該類載入器的父類載入器
ClassLoader getParent()
// 連結指定的類。類載入器可以使用此方法來連結類 c
protected void resolveClass(Class<?> c)
// 如果此 Java 虛擬機器已經載入了名為 name 的類,則直接返回該類對應的 Class 例項,否則返回 null。該方法是 Java 類載入快取機制的體現
protected Class<?> findLoadedClass(String name)

使用自定義的類載入器,可以實現如下常見功能:

  • 執行程式碼前自動驗證數字簽名
  • 根據使用者提供的密碼解密程式碼,從而可以實現程式碼混淆器來避免反編譯 *.class 檔案
  • 根據使用者需求來動態地載入類
  • 根據應用需求把其他數字以位元組碼的形式載入到應用中

URLClassLoader 類

Java 為 ClassLoader 提供了一個 URLCLassLoader 實現類,該類也是系統類載入器和擴充套件類載入器的父類。URLCLassLoader 功能強大,它既可以從本地檔案系統獲取二進位制檔案來載入類,也可以從遠端主機獲取二進位制檔案來載入類。

。。。

通過反射檢視類資訊

載入到應用中

URLClassLoader 類

Java 為 ClassLoader 提供了一個 URLCLassLoader 實現類,該類也是系統類載入器和擴充套件類載入器的父類。URLCLassLoader 功能強大,它既可以從本地檔案系統獲取二進位制檔案來載入類,也可以從遠端主機獲取二進位制檔案來載入類。

。。。

相關文章