JAVA 類加載機制學習筆記

JAVA 類生命週期

 

  如上圖所示,Java類的生命週期如圖所示,分別爲加載、驗證、準備、解析、初始化、使用、卸載。其中驗證、準備、解析這三個步驟統稱爲連接。java

  加載:JVM根據全限定名來獲取一段二進制字節流,將二進制流轉化爲方法區的運行時數據結構,在內存中生成一個表明該類的Java.lang.Class對象,做爲方法區這個類的各類數據訪問入口。數據結構

  驗證:驗證是連接的第一步,主要驗證內容分爲文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。spa

  準備:準備階段是爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。線程

  解析:解析階段是虛擬機將常量池的符號引用替換爲直接引用的過程。符號引用所引用的目標不必定在內存中,而轉換爲直接引用以後,引用直接指向到對應的內存地址。設計

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

  通過初始化以後,Java類已經加載完畢。code

  

  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的方法句柄,若是對應的類沒有初始化、則會被初始化。

 

  常在筆試題中遇到的就是類記載相關知識,以下面代碼,先不看答案想一想會打印出什麼blog

例子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     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     }

  二、非靜態代碼:

 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行。因此最先打印出二、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環境下規則也不大同樣。

相關文章
相關標籤/搜索