4. 虛擬機器類載入機制 (一)
1.概述
上節講到虛擬機器要載入Class檔案,那是如何載入這些Class檔案呢?Class檔案中的資訊進入到虛擬機器會發生哪些變化?都是這節要討論的。
2. 什麼是虛擬機器的類載入機制
虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別,這就是虛擬機器的類載入機制。
3. 在java語言中類的載入、連線和初始化過程都是在程式執行期間完成的,這樣做可以為java應用程式提過高度的靈活性。java裡天生可以動態擴充套件的語言特性就是依賴執行期動態載入和動態連線這個特點實現的。例如,編寫一個面向介面的應用的程式,可以等到執行時再指定其實際的實現類;還可以讓本地應用程式在執行時從網路或其他地方載入一個二進位制流作為程式碼的一部分,這種組裝技術以廣泛應用於java程式中。
4. 約定:本節我們提到的 Class檔案並非特指存在於具體磁碟中的檔案,應當是一串二進位制的位元組流。
5. 有且只有5種情況必須對類進行“初始化”:
(1)遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,先對類進行初始化。最常見的java程式碼場景是:使用new關鍵字例項化物件的時候;讀取或設定一個類的靜態欄位(被final修飾、編譯器放入常量池的靜態欄位除外);呼叫一個類的靜態方法的時候。
(2)使用java.lang.reflect包的方法對類進行反射呼叫的時候,先進行類的初始化
(3)初始化一個類時,如果父類還沒初始化,先進行父類的初始化
(4)當虛擬機器啟動時,使用者指定一個要執行的主類(就是包含main方法的那個類),先對主類進行初始化
(5)沒見過,這個不太懂
關於介面的初始化時機與類的時機區別僅僅上面5中情況的第3種:介面初始化時,並不要求其父介面全部完成了初始化,只有在真正使用到父介面的時候才會初始化。
“被動引用”情況下不會觸發類的初始化:
(1)通過子類引用父類的靜態欄位,不會導致子類初始化。對於靜態欄位,只有直接定義這個欄位的類才會被初始化。
(2)通過陣列定義來引用類,則不會觸發此類的初始化
(3)常量(就是被final修飾的量)在編譯階段會被放入類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發類的初始化
6. 類的生命週期
類的生命週期包括: 載入--驗證--準備--解析--初始化--使用--解除安裝
載入
在載入階段,虛擬機器要完成以下3件事情:
1)通過一個類的全限定名來獲取定義此類的二進位制位元組流。
1.1 從哪獲取?
沒有明確規定,可以從ZIP包中讀取、從網路中讀取、執行時計算生成、由其他檔案生成(如JSP對應的Class 類)、從資料庫中讀取。
1.2 這個動作在java虛擬機器外部去實現,實現這個動作的程式碼模組稱為“類載入器”。
2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口
在載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區中,然後在記憶體中例項化一個 java.lang.Class類的物件,這個物件將作為程式訪問方法區中這些型別資料的外部介面。
這裡還需介紹下陣列類的載入,陣列類不通過類載入器建立,它是由java虛擬機器直接建立的。但是陣列類的元素型別最終還是要考類載入器去建立。
驗證:非常重要但不是一定必要的階段
驗證是目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
驗證大致完成4個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證
檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本虛擬機器處理。此階段目的是保證輸入的位元組流能正確解析並存儲於方法區內,格式上符合一個java型別資訊的要求。此階段的驗證是基於二進位制位元組流進行的,此階段後,位元組流進入方法區,此後的驗證均基於方法區的儲存結構,不會再直接操作位元組流。
元資料驗證:對位元組碼描述的資訊進行語義分析,以保證其符合java語言規範的要求。
這個類是否有父類、類的父類是否繼承了不允許繼承的類、非抽象類是否實現了父類或介面中要求的全部方法、與父類是否矛盾。此階段主要目的是對類的元資料資訊進行語義校驗,保證不存在不符合java語言規範的元資料資訊。
位元組碼驗證:主要目的是通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。這是對類的方法體進行校驗分析。這裡需要注意:即使一個方法體通過了位元組碼驗證,也不能確定其一定就是安全的。-不能通過程式準確地檢查出程式是否能在有限的時間內結束執行。
符號引用驗證:可以看做是對類自身以外的資訊(常量池中的各種符號引用)進行匹配性校驗。如:符號引用中通過字串描述的全限定名是否能找到對應的類、符號引用中的類、欄位、方法的訪問性是否可以被當前類訪問。 此階段的目的是確保解析動作能正常執行。
準備
準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數使用的記憶體均在方法區中進行分配。這裡需注意:這裡進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數在java堆中分配。其次,這裡說的“類變數初始值”通常為資料型別的零值,例如一個類變數:public static int value = 123; 在準備階段後,初始值為0而不是123;那不通常的情況是什麼? 是final型別的值:public static final int value=123; 在準備階段直接初始為123。這就貌似能解釋,在呼叫類的常量(final型別)時不會引起類的初始化,它直接在準備階段就拿到了123,而類的靜態變數初始值為0,所以在呼叫靜態變數時會發生類的初始化,初始化為123.
解析
解析階段是虛擬機器將常量池中的符號引用替換為直接引用的過程
符號引用:符號引用以一組符號來描述所引用的目標。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經記載到記憶體中。對同一個符號引用進行多次解析請求是很常見的事情。
直接引用:可以是直接指向目標的指標、相對偏移量或一個能間接定位到目標的控制代碼。直接引用與虛擬機器實現的記憶體佈局有關,直接引用的目標必定已經在記憶體中存在。
解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行,我們只介紹前4種。
1 、類或介面的解析:如果當前程式碼所處類為D,如果要解析一個符號引用為N,解析為一個類或介面C的直接引用,需完成3個步驟:1) 如果C不是一個數組型別,那虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入這個類C。在載入過程中,由於元資料驗證、位元組碼驗證的需要,又可能觸發其他類的載入動作。一旦載入過程出現任何異常,解析宣告失敗。2)如果C是一個數組型別,並且陣列元素型別為物件,那將按照1)的規則載入陣列元素型別,接著虛擬機器生成一個代表此陣列唯獨和元素的陣列物件。3)如果上步沒有出錯,那麼C在虛擬機器中實際上已經成為一個有效的類或介面了,但在解析完成之前還要驗證D是否具備對C的訪問許可權,否則,也會異常。
1、類或介面的解析:判斷所要轉化成的直接引用是對陣列型別,還是普通的物件型別的引用,從而進行不同的解析。
2、欄位解析:先解析出欄位所屬的類或介面的符號引用,解析出具體的類後,會先在此類中查詢是否包含有簡單名稱和欄位描述符都與目標相匹配的欄位,如果有,則查詢結束;如果沒有,則會按照繼承關係從上往下遞迴搜尋該類所實現的各個介面和它們的父介面,還沒有,則按照繼承關係從上往下遞迴搜尋其父類,直至查詢結束。
3、類方法解析:先解析出欄位所屬的類或介面的符號引用,解析出具體的類後,如果此類是個介面,則丟擲異常,否則先在C類中查詢是否有簡單名稱和描述符都與目標相匹配的方法,然後是C類的父類,然後是類C實現的介面與他們的父介面中查詢。
4、介面方法解析:先解析出欄位所屬的類或介面的符號引用,解析出具體的介面後,如果發現是個類,則拋異常,否則再C介面中查詢簡單名稱和描述符都與目標匹配的方法,然後是它的父介面中遞迴查詢。
初始化
類初始化階段才真正開始執行類中定義的java程式程式碼,在初始化階段是執行類構造器<clinit>()方法的過程。
特點:1)<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的 。注意:靜態語句塊只能訪問到定義在靜態語句塊之前的變數,後面的訪問不到。2)父類的靜態語句塊要優先於子類的變數複製操作 ; 3)介面中不能使用靜態語句塊,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法,只有當父介面中定義的變數使用時,父接口才會初始化。 4)虛擬機器會保證一個類的<clinit>()方法在多執行緒下正確地加鎖、同步,且其他執行緒喚醒後不會再次執行<clinit>()方法,它只會初始化一次。