JAVA 類載入機制學習筆記
JAVA 類生命週期
如上圖所示,Java類的生命週期如圖所示,分別為載入、驗證、準備、解析、初始化、使用、解除安裝。其中驗證、準備、解析這三個步驟統稱為連結。
載入:JVM根據全限定名來獲取一段二進位制位元組流,將二進位制流轉化為方法區的執行時資料結構,在記憶體中生成一個代表該類的Java.lang.Class物件,作為方法區這個類的各種資料訪問入口。
驗證:驗證是連結的第一步,主要驗證內容分為檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。
準備:準備階段是為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。
解析:解析階段是虛擬機器將常量池的符號引用替換為直接引用的過程。符號引用所引用的目標不一定在記憶體中,而轉換為直接引用之後,引用直接指向到對應的記憶體地址。
初始化:初始化會執行<Clinit>()方法,會對static屬性進行賦值,對於static final修飾的基本型別和String型別則在更早的javac編譯的時候已經載入到常量池中了。
經過初始化之後,Java類已經載入完畢。
JVM並沒有強制規定什麼時候進行類的載入,但是對於初始化規定了有且5種情況必須被初始化:
- 遇到new、getstatic、putstatic、invokestatic這四個位元組碼的執行時,如果類還沒有被初始化,則必須被初始化。new為建立物件,剩下三個為操作靜態變數。
- 使用java.lang.reflect對類進行反射操作的時候,如果該類還沒有被載入,則載入該類。
- 如果對一個類進行初始化的時候,要先對其父類先進行初始化。
- 當虛擬機器啟動的時候,需要一個Main方法入口,虛擬機器會先初始化這個類。
- 當使用JDK1.7動態語言支援的時候,如果一個java.lang.invoke.MethodHandle例項最終解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,如果對應的類沒有初始化、則會被初始化。
常在筆試題中遇到的就是類記載相關知識,如下面程式碼,先不看答案想想會打印出什麼
例子1:
1 public class SuperClass { 2 public static int value = 123; 3 4 static { 5 System.out.println("super class init"); 6 } 7 } 8 9 public class SubClass extends SuperClass { 10 11 static { 12 System.out.println("Sub class init"); 13 } 14 } 15 16 public class ClassInit { 17 public static void main(String[] args) { 18 System.out.println(SubClass.value); 19 } 20 }
列印結果如下:
解析:當Main方法執行的時候,不會對SubClass類進行初始化,因為呼叫靜態變數時只會初始化直接定義該變數的類,因此,上述程式碼只有SuperClass會被初始化。而SubClass並不會被初始化。
我們稍微修改一個上述程式碼,將main方法放入子類中執行,執行main方法之後,程式碼會怎麼執行呢?
例子2:
1 public class SuperClass { 2 public static int value = 123; 3 4 static { 5 System.out.println("super class init"); 6 } 7 } 8 9 public class SubClass extends SuperClass { 10 11 static { 12 System.out.println("Sub class init"); 13 } 14 15 public static void main(String[] args) { 16 System.out.println(SubClass.value); 17 } 18 }
列印如下圖:
解析:根據上述類初始化規定。根據第四條執行main方法時候必須初始當前類,因此觸發了SubClass的初始化。根據第三條,如果要觸發SubClass,必須先對SuperClass進行初始化。因此會先進行SuperClass的初始化、執行完成後執行SubClass初始化,最後等SubClass初始化完畢,打印出Main方法的中的語句。
例子3:
1 public class StaticTest { 2 3 static int b = 200; 4 5 static StaticTest st = new StaticTest(); 6 7 static { 8 System.out.println("1"); 9 } 10 11 { 12 System.out.println("2"); 13 } 14 15 StaticTest() { 16 System.out.println("3"); 17 System.out.println("a=" + a + ",b=" + b+",c="+c+",d="+d); 18 } 19 20 public static void staticFunction() { 21 System.out.println("4"); 22 } 23 24 int a = 100; 25 26 static int c = 300; 27 28 static final int d=400; 29 30 public static void main(String[] args) { 31 staticFunction(); 32 } 33 }
執行結果如下:
分析:程式碼執行之後的結果跟我一開始預想的不大一樣,我們按照執行順序進行分析。當我們執行Main方法的之前,javac需要先將程式碼編譯,在這個時候d屬性已經完成了賦值。前面說過,在執行main方法之前,會對main方法所在的類進行初始化。根據屬性是否靜態,我們大概可以將程式碼分為兩部分:
1、靜態程式碼
1 static int b = 200; 2 3 static StaticTest st = new StaticTest(); 4 5 static { 6 System.out.println("1"); 7 } 8 public static void staticFunction() { 9 System.out.println("4"); 10 }
2、非靜態程式碼:
1 { 2 System.out.println("2"); 3 } 4 5 StaticTest() { 6 System.out.println("3"); 7 System.out.println("a=" + a + ",b=" + b + ",c=" + c + ",d=" + d); 8 } 9 10 int a = 100;
把程式碼分成兩部分,主要是為了區分哪些是類初始化裡的程式碼(<clinit>()中的程式碼,在類初始化的時候執行),哪些物件初始化程式碼(<init>()中的程式碼,物件初始化的時候執行)。main方法觸發了類的初始化,因此會執行<clinit>()中的程式碼,執行順序從上而下,先完成b=200賦值語句,緊接著執行 static StaticTest st = new StaticTest(),而對st的賦值則觸發了物件初始化方法,因此會執行<init>()方法,即非靜態程式碼,物件的初始化執行順序和類初始化執行順序不相同,類初始化執行順序 屬性初始化 =》程式碼塊 =》方法初始化。因此在非靜態程式碼中執行順序為: 第10行=》第2行=》第6行=》第7行。所以最早打印出2、3。緊接著列印a、b、c、d數值的時候a、b、d已經完成賦值。完成物件初始化之後,繼續執行上面的靜態程式碼,打印出1。等類已經完成了載入,執行main方法,打印出4。
雙親委派模型
在java類載入器對Class進行載入的時候,如果兩個類被不同的類載入器載入,則這兩個類不相等。通過equals(),instanceof等方法判斷結果為false。關於Java的類載入器,大概可以劃分為以下幾種:
- 啟動類載入器(Bootstrap ClassLoader):Bootstrap ClassLoader是唯一一個通過JVM內部的類載入器,通過C++實現。負責載入<JAVA_HOME>/lib中,或者被-Xbootclasspath引數所指定的路徑中的,或者被虛擬機器識別的類庫記載到虛擬機器記憶體中,僅按照檔名識別,如rt.jar,名字不符合的類庫即使放在lib中也不會被載入。啟動類載入器無法被Java程式直接使用,如果需要把載入器請求委派給引導類載入,則直接使用null代替即可。
- 擴充套件類載入器(Extension ClassLoder):這個載入器負責載入<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。
- 應用程式類載入器(Application ClassLoader):應用程式類載入器,負責載入使用者路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自義定類載入,則該類載入就是預設的類載入器。
我們的應用程式大部分是通過上面三種類載入器配合完成的,如果有特殊需求,還可以自定義自己的類載入器。包括類記載器在內,各種類載入器的關係可以這樣表示。
上述關係圖稱為雙親委派模型,除了Bootstrap ClassLoad載入器,其他的類載入器都有自己的父類。當一個類載入器獲取到一個類載入任務時,先將該類丟給父載入器載入, 如果父載入器不能載入則自己載入。這種載入方式就稱之為雙親委派。如上圖所示,自定義類載入器獲取到一個載入任務,一層層往上丟,所以最先讓啟動類載入器載入,如果啟動類載入器能載入,則啟動類載入器載入,啟動類載入器不能載入,則丟給擴充套件類載入器,如果擴充套件類載入器不能載入,則丟給應用類載入器,如果應用類載入器不能載入,才丟給自定義載入器載入。
上述的載入方式看起來特別麻煩,但是卻解決了一個很重要的問題。比如自定義類載入器獲取到一個Java.lang.Object的任務,則讓Bootstrap ClassLoader載入,否則如果使用者自己定義了一個Java.lang.Object會跟rt.jar中的類產生衝突,通過雙親委派模型,則使用者自己寫的Object將永遠不會被載入到。
雙親委派模型是Java虛擬機器推薦給開發者的類載入實現,並不是一個強制性約束。在一些情況下雙親委派模型是會被破壞的,比如為了載入JNDI提供者的程式碼,設計出來的執行緒上下文載入器。又比如OSGI環境下規則也不大一樣。