類加載程序
- 一. 類加載程序
- 二. 加載
- 1. 加載程序
- 2. 加載class檔案的方式
- 三. 連接
- 1. 驗證
- 2. 準備
- 3. 決議
- 1. 類或介面的決議
- 2. 欄位的決議
- 3. 方法決議
- 1. 類方法決議
- 2. 介面方法決議
- 四. 初始化
- 什么時候類會被初始化:
一. 類加載程序
類加載程序可以分為三個階段
- 加載(Loading)
- 連接(Linking)
a. 驗證(Verify)
b. 準備
c. 決議 - 初始化(Initialization)
下面這圖是類的生命周期圖,這里只看初始化以及之前的程序

二. 加載
1. 加載程序
- 通過一個類的全限定名獲取定義此類的二進制位元組流
- 通過類加載器 將這個位元組流所代表的靜態存盤結構轉化為方法區(1.8后為元空間)的運行時資料結構
- 在記憶體中生成代表一個類的java.lang.class物件,作為方法區里面這個類的資料訪問入口
2. 加載class檔案的方式
- 從本地系統中直接加載
- 通過網路獲取,典型場景:Web Applet
- 從zip壓縮包中讀取,成為日后jar、war格式的基礎
- 運行時計算生成,使用最多的是:動態代理技術
- 由其他檔案生成,典型場景:JSP應用從專有資料庫中提取.class檔案
- 從加密檔案中獲取,典型場景:防Class檔案被反編譯的保護措施
三. 連接
1. 驗證
驗證是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機的要求,不會威脅到jvm的安全
驗證主要包括以下幾個方面的驗證:
-
class檔案格式的驗證 (驗證位元組流是否符合Class檔案的規范,是否能被當前版本的虛擬機處理)
- 是否以0xCAFEBABE開頭,
- 主、次版本是否在當前虛擬機處理的范圍之內
- 常量池的常量是否有不支持的常量型別
…等等,主要的目的是保證輸入的位元組流能夠正確的決議并存盤于方法區之內,格式上符合一個java型別資訊的要求,只有通過這個階段的驗證,虛擬機才會讓位元組流進入到方法區中進行存盤,后面的驗證都是直接操作在方法區之上,而不是直接操作位元組流
-
元資料驗證 (對位元組碼描述的資訊進行語意分析,確保符合java語言規范)
- 這個類是否有父類,java中除了Object,其它的類必須存在父類,默認為Object
- 這個類是否extends了不被允許的類,如被final修飾的類
- 如果這個類不是抽象類,是否實作了其父類或介面之中需要實作的所有方法
- 類中的欄位、方法是否與父類產生矛盾,如覆寫了父類的final欄位和方法,覆寫不符合規則等等
-
位元組碼驗證 (通過資料流和控制流分析,確定語意是合法的,符合邏輯的)
- 保證任意時刻運算元堆疊的資料型別與指令碼序列都能夠配合作業,不會出現類似于在操作堆疊放置了 一個int型別的資料,使用時卻按long型別來載入本地變數表中
- 保證跳轉指令不會跳到方法體以外的位元組碼指令上
- 保證型別轉換是有效的,例如把父型別賦值給子型別是安全的,子型別賦值給父型別就是不安全危險的
-
符號參考驗證 (對除了自身類,以外的各類資訊進行匹配性校驗,確保決議行為可以正常執行)
- 符號參考中通過字串的描述的全限定名是否能找到對應的類
- 在指定類中是否符合方法的欄位描述及簡單名稱所描述的方法和欄位
- 在符號參考中的類,欄位,方法的可訪問性是否可被當前類訪問
2. 準備
準備階段是正式為類實體變數分配記憶體并且設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配,這里的類變數指的是被static修飾的變數,不包括實體變數,實體變數將會在物件實體化的時候隨著物件一起分配在java堆中,另外這里的初始值并不是代碼中 = 右邊的初始值而是變數資料型別對應的零值(int為0,String為null等)
如下面的例子:這里在準備階段過后的初始值為0,而不是7
public static int a=7
3. 決議
決議階段是將常量池中的符號參考替換為直接參考的程序,決議動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法句柄和呼叫點限定符7類符號參考進行,分別對應于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 7種常量型別
符號參考以一組符號來描述所參考的目標,符號可以是任何形式的字面量,只要可以在使用時能無任何歧義的定位到目標時即可
直接參考是指向目標的指標、相對偏移量或者是一個可以間接可以定位到目標的句柄
1. 類或介面的決議
假設當前代碼所處的類為D,如果要把一個從未決議過的符號參考N決議為名叫C的類或介面的直接參考,那虛擬機完成整個決議的程序需要以下3個步驟:
- 如果C不是一個陣列型別,那虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類C,在加載程序中,由于元資料驗證、位元組碼驗證的需要,又可能觸發其他相關類的加載動作,例如加載這個類的父類或實作的介面,一旦這個加載程序出現了任何例外,決議程序就宣告失敗,
- 如果C是一個陣列型別,并且陣列的元素型別為物件,也就是N的描述符會是類似“[Ljava/lang/Integer”的形式,那將會按照第1點的規則加載陣列元素型別,如果N的描述符如前面所假設的形式,需要加載的元素型別就是“java.lang.Integer”,接著由虛擬機生成一個代表此陣列維度和元素的陣列物件,
- 如果上面的步驟沒有出現任何例外,那么C在虛擬機中實際上已經成為一個有效的類或介面了,但在決議完成之前還要進行符號參考驗證,確認D是否具備對C的訪問權限,如果發現不具備訪問權限,將拋出java.lang.IllegalAccessError例外,
加載成功后:
- 類C 加載成功后,會創建一個InstanceKlass物件,InstanceKlass存放在元空間
- 類C 實體化出來的物件則對應InstanceOopDesc,InstanceOopDesc存放在堆
類的決議是將一個類的符號參考改為指向InstanceKlass物件的直接指標(每個InstanceKlass物件表示一個具體的Java類,這個Java類不包括Java陣列),指向這個物件的開頭, 當創建物件的時候,這個指標會賦值給物件頭中_kclass指標, 這樣就定位到了該類的資料,訪問類的元資料資訊,是通過描述該類的類的物件實作的,當然 每個類只對應一個InstanceKlass物件,這就是類本身如何被描述的記憶體形態,
因為物件內部的資料在記憶體中的連續堆放的,當你訪問一個類的某欄位,是需要通過元資料InstanceKlass物件 記錄的這個欄位與物件頭的偏移量來獲取, 當然呼叫物件的方法是定位到虛方法表 而不是定位到物件的記憶體區域
創建物件其實就是僅僅向一塊記憶體區域寫入與類元資料對應的各種欄位的值,當然物件型別的值是一個參考,訪問一個物件的欄位的值, 是通過定位這個欄位在這個物件起始地址的相對偏移量,確定相對偏移量就是在欄位決議階段完成的,
2. 欄位的決議
欄位的決議是確定一個物件的欄位的訪問地址,是計算相對物件起始地址的偏移量
要決議一個未被決議過的欄位符號參考,首先會決議所屬的類或介面的符號參考,將這個欄位所屬的類或介面用C表示,如果這個類決議完成,虛擬機規范要求按照如下步驟對C進行后續欄位的搜索
- 如果C本身就包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則回傳這個欄位的直接參考,查找結束,
- 否則,如果在C中實作了介面,將會按照繼承關系從下往上遞回搜索各個介面和他的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則回傳這個欄位的直接參考,查找結束,
- 否則,如果C不是java.lang.Object的話,將會按照繼承關系從下往上遞回搜索其父類,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則回傳這個欄位直接參考,查找失敗,
- 否則,查找失敗,拋出java.lang.NoSuchFieldError例外,
出錯情況:
- 如果查找程序成功回傳了參考,將會對這個欄位進行權限驗證,如果發現不具備對欄位的訪問權限,將拋出java.lang.IllegalAccessError例外
- 如果 C 的父類和所實作的介面中都有這個欄位
- 如果C 也有這個欄位,虛擬機會確定C的這個欄位就是訪問欄位
- 如果C沒有這個欄位,虛擬機無法確定訪問的欄位到底是介面的,還是父類的,javac編譯器將提示
The field xx is ambiguous
3. 方法決議
1. 類方法決議
類方法決議的第一個步驟與欄位決議一樣,也需要決議所屬的類或介面的符號參考,我們依然用C表示這個類,如果決議成功:
- 類方法和介面方法符號參考的常量型別定義是分開的,如果在類方法表中發現class_index中索引的C是個介面,那就直接拋出java.lang.IncompatibleClassChangeError例外,
- 如果通過了第1步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束
- 否則,在類C的父類中遞回查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束
- 否則,在類C實作的介面串列及他們的父介面之中遞回查找是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象,這時查找結束,拋出java.lang.AbstractMethodError例外
- 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError
最后,如果查找程序成功回傳了直接參考,將會對這個方法進行權限驗證,如果發現不具備對此方法的訪問權限,將拋出java.lang.IllegalAccessError例外
2. 介面方法決議
介面方法決議的第一個步驟與欄位決議一樣,也需要決議所屬的類或介面的符號參考,如果決議成功,依然用C表示這個介面,接下來虛擬機將會按照如下步驟進行后續的介面方法搜索:
-
與類方法決議不同,如果在介面方法表中發現class_index中的索引C是個類而不是介面,那就直接拋出java.lang.IncompatibleClassChangeError例外
-
否則,在介面C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束
-
否則,在介面C的父介面中遞回查找,直到java.lang.Object(查找范圍會包括Object類)為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束
-
否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError例外
由于介面中的所有方法默認都是public,所以不存在訪問權限的問題,因此介面方法的符號決議應當不會拋出java.lang.IllegalAccessError例外
每一個類加載后,會對應一個虛方法表
當第一次呼叫方法時,也就是執行invokevirtual指令,指令引數為 該方法的符號參考(包含了引數個數和型別資訊,回傳值型別,這樣就區分了方法多載是不同的方法),也就是對應找到常量表中的methodref型別的項,(class檔案中不同型別的項都有標記來標識,從而能夠描述并得到這個的項的內部結構 而取到對應的值),
在虛方法表,根據方法描述符找到對應指向匹配方法的下標,該下標指向methodblock*指標,也就是對應的方法記憶體地址入口,然后把虛方法表的下標和引數個數 寫回到該型別為methodref的常量池項 比如是第二項#2,來取代之前的符號參考,也就是說符號參考變成了虛方法表的下標,這個下標就是一種直接參考的體現
類的直接參考–> ClassClass–> methodtable - 下標 -> methodblock結構體(ClassClass)
第二次呼叫方法,這時候invokevirtual指令會變成invokevirtual_quick, 該指令的引數為虛方法表的下標(vtable index)和 方法的引數個數, 所以呼叫方法并不是直接呼叫方法塊,而是先找到虛方法表,再去根據下標呼叫對應的方法塊
四. 初始化
初始化階段就是執行類構造器法<clinit>()的程序
<clinit>()方法是由編譯器自動收集類中的所有類變數(靜態變數)的賦值動作和靜態陳述句塊(static{})中的陳述句合并產生的,編譯器收集的順序是由陳述句在源檔案中出現的順序決定的,靜態陳述句塊只能訪問到定義在靜態陳述句塊之前的變數,定義在它之后的變數,靜態陳述句塊能進行賦值操作,但是不能進行訪問
- <clinit>()方法與類的建構式不同,它不需要顯式的呼叫父類構造器,虛擬機會保證在<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢,因此在虛擬機中第一個被執行的<clinit>()方法肯定是java.lang.Object的<clinit>()方法
- <clinit>()方法對于類或介面來說并不是必需的,如果一個類中沒有靜態陳述句塊,也沒有靜態變數的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法
- 虛擬機會保證一個類的<clinit>()方法在多執行緒環境中被正確的加鎖、同步,如果多個執行緒去初始化一個類,那么只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒的<clinit>()方法執行完畢,同一個虛擬機上類的<clinit>()方法只會執行一次
什么時候類會被初始化:
- 實體化一個類,new一個類的實體物件
- 訪問類的靜態變數
- 呼叫類的靜態方法
- 通過反射呼叫類
- 實體化類的子類
- 被標位啟動類的類(即main方法執行的類)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/273335.html
標籤:其他
上一篇:JAVA 多執行緒
