類別載入會將二進位表示轉換為 Class 物件。連結與初始化是分開的步驟,因此載入型別不代表其靜態初始化器已經執行。
載入器與型別識別 #
JDK 提供 bootstrap、platform 與 application 載入器。bootstrap 載入器定義核心平台類別,並在選定的 API 中以 null 表示;platform 載入器定義平台模組;application 載入器從 class path 或 module path 載入應用程式類別。自訂載入器通常使用 parent-first 委派:
- 重複使用已載入的定義;
- 要求父載入器載入二進位名稱;接著
- 尋找位元組並呼叫
defineClass。
Parent-first 委派能保護平台型別識別。它是慣例而非絕對規則:外掛系統有時會使用 child-first 載入,並嚴格排除特定套件。執行期型別同時由其二進位名稱與定義它的載入器識別。因此,兩個載入器可以定義不同的 com.example.Plugin 型別,且實例不能在兩者之間轉型。
載入、連結與初始化 #
JVM 規格將生命週期區分如下:
- 載入取得位元組、建立
Class物件,並建立定義載入器的關聯。 - 連結包括驗證、準備與選擇性解析。驗證會檢查結構與型別安全限制。準備會配置靜態欄位並指派其預設值。解析會將符號參照轉換成執行期參照;實作可以延後解析。
- 初始化會執行合成的
<clinit>方法,其中包含靜態欄位初始化器與依原始碼順序排列的靜態區塊。
編譯期常數會在準備階段指派,因此讀取常數變數可能避免初始化其宣告類別。不要從實作的記錄輸出推論可移植的解析排程。
確切的初始化觸發條件 #
JLS §12.4.1 要求在類別首次被主動使用前立即初始化:建立實例、呼叫靜態方法、指派靜態欄位,或讀取不是常數變數的靜態欄位。它也包括執行需要該類別的 static 斷言、選定的反射操作,以及指定的方法控制代碼行為。Class.forName 預設會初始化;使用三個參數的多載並傳入 false,即可在不要求初始化的情況下載入。
類別 C 初始化前,其超類別會先初始化;接著在適用時,依 JLS 定義的順序初始化宣告預設方法的超介面。初始化類別不會初始化它實作的任意介面。讀取 Child.value 會初始化實際宣告 value 的類別,不一定是 Child。
失敗與並行 #
JVM 會針對每個類別或介面序列化初始化。某個執行緒執行 <clinit> 時,其他執行緒會等待;同一執行緒提出的遞迴要求則允許通過。若初始化異常完成,VM 會將型別記錄為錯誤,向觸發使用回報初始化失敗,之後的主動使用會以 NoClassDefFoundError 失敗。若要取得另一個初始化嘗試,必須重新啟動程序或使用獨立載入器。
靜態初始化器應建立簡單且不會失敗的不變量。網路呼叫、鎖取得與設定剖析可能造成死結,或在一次暫時性失敗後永久毒化型別。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)