1. 程式人生 > >JAVA 類載入機制學習筆記

JAVA 類載入機制學習筆記

JAVA 類生命週期

 

  如上圖所示,Java類的生命週期如圖所示,分別為載入、驗證、準備、解析、初始化、使用、解除安裝。其中驗證、準備、解析這三個步驟統稱為連結。

  載入:JVM根據全限定名來獲取一段二進位制位元組流,將二進位制流轉化為方法區的執行時資料結構,在記憶體中生成一個代表該類的Java.lang.Class物件,作為方法區這個類的各種資料訪問入口。

  驗證:驗證是連結的第一步,主要驗證內容分為檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。

  準備:準備階段是為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。

  解析:解析階段是虛擬機器將常量池的符號引用替換為直接引用的過程。符號引用所引用的目標不一定在記憶體中,而轉換為直接引用之後,引用直接指向到對應的記憶體地址。

  初始化:初始化會執行<Clinit>()方法,會對static屬性進行賦值,對於static final修飾的基本型別和String型別則在更早的javac編譯的時候已經載入到常量池中了。

  經過初始化之後,Java類已經載入完畢。

  

  JVM並沒有強制規定什麼時候進行類的載入,但是對於初始化規定了有且5種情況必須被初始化:

  1. 遇到new、getstatic、putstatic、invokestatic這四個位元組碼的執行時,如果類還沒有被初始化,則必須被初始化。new為建立物件,剩下三個為操作靜態變數。
  2. 使用java.lang.reflect對類進行反射操作的時候,如果該類還沒有被載入,則載入該類。
  3. 如果對一個類進行初始化的時候,要先對其父類先進行初始化。
  4. 當虛擬機器啟動的時候,需要一個Main方法入口,虛擬機器會先初始化這個類。
  5. 當使用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環境下規則也不大一樣。