1. 程式人生 > >12 類的載入過程

12 類的載入過程

《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第2版)》7.3節 類的生命週期:
載入、驗證、準備、初始化和解除安裝5個階段必須按順序開始,不一定按順序進行、結束 這些階段通常是交叉進行的(在一個階段執行過程中呼叫、啟用另外一個階段) 解析階段在某些情況下可在初始化階段後開始,目的是為了支援Java語言的執行時繫結(也稱動態繫結、晚期繫結)

1. 載入

1)通過類全限定名獲取定義該類的二進位制位元組流 未限定必須從檔案獲取 可從壓縮包獲取,例JAR、WAR 可在執行時計算生成,例動態代理,java.lang.reflect.Proxy用ProxyGenerator.generateProxyClass為介面生成形式為"*$Proxy"代理類的二進位制位元組流 可從網路、資料庫獲取,可由其他檔案生成,例JSP應用 非陣列類可自定義類載入器控制位元組流生成方式,即重寫載入器loadClass() 陣列類本身不通過載入器建立,由Java虛擬機器直接建立,但陣列中的元素(類)由類載入器建立 若陣列元件(解釋見文末)為引用型別,則遞迴採用本節定義的載入過程載入該元件型別,陣列將在載入該元件型別的類載入器的類名稱空間上被標識(這點很重要,一個類必須與類載入器一起確定唯一性) 若陣列元件型別不是引用型別(例int[]陣列),則陣列被標記為與引導類載入器關聯 2)將位元組流中的靜態儲存結構轉化為方法區執行時資料結構 3)在記憶體(方法區)中生成代表該類的java.lang.Class物件,作為該類的資料訪問入口

2. 連線第一步,驗證

目的:為確保Class檔案位元組流中包含的資訊符合當前虛擬機器要求,且不會危害虛擬機器自身安全 該階段工作量在虛擬機器類載入子系統中佔相當大一部分 若驗證到輸入位元組流不符合Class檔案格式約束,虛擬機器丟擲java.lang.VerifyError異常或其子類異常 4個子階段(《Java虛擬機器規範(JavaSE7版)》起): 1). 檔案格式驗證 驗證位元組流是否符合Class檔案格式規範,且能被當前版本虛擬機器處理 基於二進位制位元組流進行,通過該子階段驗證後位元組流才會儲存到方法區中進行儲存,以後3個子階段全部基於方法區儲存結構進行 驗證點:魔數,主、次版本號,常量池常量,各種索引值,等 2). 元資料驗證 對位元組碼描述資訊進行語義分析,保證其描述的資訊符合Java語言規範要求 校驗的是元資料的資料型別 驗證點:是否有父類,是否繼承了不允許被繼承的類,非抽象類是否實現了其父類或介面要求實現的所有方法,類中欄位、方法是否與父類產生矛盾,等 3). 位元組碼驗證 通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的 最複雜的子階段 驗證點:保證跳轉指令不會跳轉到方法體以外位元組碼指令,保證方法體中的型別轉換有效,等 4). 符號引用驗證 確保解析動作能正常執行,可看做是對類自身以外(常量池中各種符號引用)資訊進行匹配性校驗 發生在虛擬機器將符號引用轉化為直接引用時,該轉化動作在連線第三步,解析階段中發生 重要但非必要階段,可用-Xverify:none關閉大部分類驗證措施 驗證點:符號引用中通過字串描述的全限定名是否能找到對應類,在指定類中是否存在符合方法的欄位描述符及簡單名稱所描述的方法和欄位,符號引用中類、欄位、方法的訪問性(private、protected、public、default)是否可被當前類訪問,等 無法通過該子階段驗證會丟擲java.lang.IncompatibleClassChangeError異常子類,例java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等

3. 連線第二步,準備

正式為類變數(static修飾的變數)分配記憶體並設定類變數初始值(零值或程式碼定義值),這些變數使用的記憶體都在方法區分配 有static、無final修飾的變數,初始值為零值,為變數賦程式碼中的值操作在類構造器<clinit>()方法中 有static、有final修飾的變數,初始值為程式碼中的值,該類欄位在欄位屬性表中有ConstantValue屬性 基本資料型別的零值:

4. 連線第三步,解析

虛擬機器將常量池內符號引用替換為直接引用 虛擬機器規範中未規定解析階段具體發生時間,只要求在執行new、getfield、invokeinterface、invokestatic、invokedynamic等16個用於操作符號引用的位元組碼指令前,先對使用的符號引用進行解析 虛擬機器會保證,在同一個實體中,若符號引用成功解析過,則後續解析應一直成功,若第一次解析失敗,則後續解析應收到相同的解析異常 除invokedynamic指令以外,虛擬機器實現可快取第一次解析結果(在執行時常量池中記錄直接引用,並將常量標識為已解析狀態)避免重複解析 invokedynamic指令的目的是用於動態語言支援(目前僅使用Java語言不會生成這條位元組碼指令),對應引用稱為“動態呼叫點限定符”(Dynamic Call Site Specifier),“動態”的含義是必須等到程式實際執行到該指令時,解析動作才能進行 除invokedynamic外可觸發解析的指令都是“靜態”的,可在剛完成載入未開始執行程式碼時即進行解析 解析動作主要針對7類符號引用:類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符,具體解析過程略

5. 初始化

在該階段之前的類載入過程中,除載入子階段,使用者應用程式可通過自定義類載入器參與類載入外,其餘動作完全由虛擬機器主導和控制 該階段真正開始執行類中定義的Java程式碼(或者說位元組碼),根據程式制定的計劃初始化類變數和其他資源 從另外一個角度表達:該階段執行類構造器<clinit>()方法 <clinit>()方法的生成: 編譯器自動收集類中所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生 編譯器收集的順序由語句在原始檔中出現的順序決定 靜態語句塊中只可訪問定義在靜態語句塊之前的變數,定義在之後的變數,靜態語句塊可賦值但不可訪問,即禁止“非法向前引用” 若類或介面中無靜態變數賦值、無靜態語句塊,則可不生成該方法 <clinit>()方法的執行: 虛擬機器會保證子類(與介面區分)<clinit>()方法執行前,父類<clinit>()方法已經執行完畢,不需顯式呼叫父類<clinit>()方法(例項構造器<init>()方法需顯式呼叫) 這意味著父類中定義的靜態變數賦值、靜態語句塊執行優先於子類 第一個被執行<clinit>()方法的類一定是java.lang.Object 介面的<clinit>()方法執行時不需要先執行父介面的<clinit>()方法,當父介面的變數被使用時才執行<clinit>()方法 虛擬機器會保證<clinit>()方法在多執行緒環境中被正確地加鎖、同步,多執行緒執行會阻塞(該阻塞較隱蔽),直到活動執行緒執行<clinit>()方法完畢

名詞解釋:

陣列元件型別:Component Type,指陣列去掉一個維度以後的型別 程式碼定義值:筆者自定義名詞,例,static value = 123; 則123即程式碼定義值 符號引用(Symbolic References): 以一組符號描述引用的目標,符號可是任何形式字面量,只要使用時能無歧義地定位到目標即可 符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中 虛擬界記憶體佈局可不同,但撻能接受的符號引用必須是一致的,因符號引用字面量形式明確定義在Java虛擬機器規範的Class檔案格式中 直接引用(Direct References): 可是直接指向目標的指標、相對偏移量或一個能間接定位到目標的控制代碼 直接引用與虛擬機器實現的記憶體佈局相關,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同 若有了直接引用,則引用的目標必定已經在記憶體中存在