Java類的載入機制(讀書筆記)
-
java類的生命週期
類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。其中驗證、準備和解析統稱為連線。
java虛擬機器並沒有強制約束在什麼情況下需要開始類載入過程的第一個階段(載入),但是嚴格規定了有且僅有5種情況必須立即對類進行初始化
- 遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令時,如果類沒有初始化,則先需要觸發其初始化。生成這4條指令的最常見的java程式碼場景是:使用new關鍵字例項化物件的時候,讀取或者設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候
- 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有過初始化,則需要先初始化其父類
- 虛擬機器啟動時,使用者需要指定一個要執行的主類,虛擬機器就會先初始化這個主類。
- 當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先出發其初始化。
以上5種場景都會觸發類的初始化,這5種場景的行為稱為對一個類進行主動引用。除此之外,其他引用類的方式都不會觸發初始化,稱之為被動引用。
被動引用例子1:通過子類引用父類的靜態欄位,只會初始化父類,而不會初始化子類
public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 123; } public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } } public class NotInitialization { public static void main(String args[]) { System.out.println(SubClass.value); } } 執行程式碼只會輸出SuperClass init,而不會輸出SubClass init
被動引用例子2:表示陣列中儲存的型別的時候不會初始化
public class NotInitialization { public static void main() { SuperClass[] sca = new SuperClass[10]; } }
被動引用例子3:被final修飾的類變數,在編譯階段已經被新增進了引用它的類的常量池中,所以對類常量的引用也不會初始化該類
public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } } 執行程式碼不會輸出ConstClass init!
-
載入:在類載入階段,虛擬機器需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區對這個類的各種資料訪問的入口。
通過一個類的全限定名獲取定義此類的二進位制位元組流有多種實現方法:
- 從zip包中讀取,這個很常見,比如jar包
- 從網路中獲取,比如Applet
- 執行時計算生成,這種場景使用的最多的就是動態代理技術
- 由其他檔案生成,典型場景就是JSP應用,即由JSP檔案生成對應的Class類
- 從資料庫中讀取
載入階段完成之後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,然後在記憶體中例項化一個java.lang.Class類的物件,這個物件將作為程式訪問方法區中這些資料的外部介面。
-
驗證:驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案位元組流中包含的資訊符合當前虛擬機器的要求,並不會危害虛擬機器的安全。驗證階段大致上會完成下面4個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。
-
準備:準備階段是正式為類變數分配記憶體並且設定類變數初始值的階段,這些類變數所使用的記憶體都將在方法區中進行分配。這時候進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化的時候隨著物件一起分配到java堆中。對類變數設定初始值是給各個類變數設定零值。
-
解析:解析階段是虛擬機器將常量池內的符號引用轉換為直接引用的過程。
-
初始化:初始化階段是類載入的最後一步。到了初始化階段,才真正開始執行類中定義的java程式碼,進行類變數和其他資源的初始化,初始化階段就是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併產生的。靜態語句塊只能訪問到定義在靜態語句之前的變數,定義在它之後的變數,在前面的靜態語句塊中可以賦值,但是不能訪問。
public class Test { static{ i = 0;//靜態變數定義到了靜態語句塊之後,可以賦值 System.out.print(i);//不能訪問,編譯器會提示“非法向前引用” } static int i = 1; }
<clinit>()方法(類的構造器函式)和類的建構函式<init>()方法不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢。由於父類的<clinit>()先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作
<clinit>()方法不是必須的,如果類或者介面中沒有靜態語句塊或者類變數,那麼編譯器可以不為這個類生成<clinit>()方法。
介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面也會生成<clinit>()方法,和類不同的是,介面執行<clinit>()方法時,沒必要先執行父介面的<clinit>()方法,只有當父介面中的變數使用時,父接口才會初始化。
虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖、同步。如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待。