如上圖所示,Java類的生命週期如圖所示,分別爲加載、驗證、準備、解析、初始化、使用、卸載。其中驗證、準備、解析這三個步驟統稱爲連接。java
加載:JVM根據全限定名來獲取一段二進制字節流,將二進制流轉化爲方法區的運行時數據結構,在內存中生成一個表明該類的Java.lang.Class對象,做爲方法區這個類的各類數據訪問入口。數據結構
驗證:驗證是連接的第一步,主要驗證內容分爲文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。spa
準備:準備階段是爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。線程
解析:解析階段是虛擬機將常量池的符號引用替換爲直接引用的過程。符號引用所引用的目標不必定在內存中,而轉換爲直接引用以後,引用直接指向到對應的內存地址。設計
初始化:初始化會執行<Clinit>()方法,會對static屬性進行賦值,對於static final修飾的基本類型和String類型則在更早的javac編譯的時候已經加載到常量池中了。3d
通過初始化以後,Java類已經加載完畢。code
JVM並無強制規定何時進行類的加載,可是對於初始化規定了有且5種狀況必須被初始化:對象
常在筆試題中遇到的就是類記載相關知識,以下面代碼,先不看答案想一想會打印出什麼blog
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方法以後,代碼會怎麼執行呢?
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方法的中的語句。
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 ClassLoad加載器,其餘的類加載器都有本身的父類。當一個類加載器獲取到一個類加載任務時,先將該類丟給父加載器加載, 若是父加載器不能加載則本身加載。這種加載方式就稱之爲雙親委派。如上圖所示,自定義類加載器獲取到一個加載任務,一層層往上丟,因此最早讓啓動類加載器加載,若是啓動類加載器能加載,則啓動類加載器加載,啓動類加載器不能加載,則丟給擴展類加載器,若是擴展類加載器不能加載,則丟給應用類加載器,若是應用類加載器不能加載,才丟給自定義加載器加載。
上述的加載方式看起來特別麻煩,可是卻解決了一個很重要的問題。好比自定義類加載器獲取到一個Java.lang.Object的任務,則讓Bootstrap ClassLoader加載,不然若是用戶本身定義了一個Java.lang.Object會跟rt.jar中的類產生衝突,經過雙親委派模型,則用戶本身寫的Object將永遠不會被加載到。
雙親委派模型是Java虛擬機推薦給開發者的類加載實現,並非一個強制性約束。在一些狀況下雙親委派模型是會被破壞的,好比爲了加載JNDI提供者的代碼,設計出來的線程上下文加載器。又好比OSGI環境下規則也不大同樣。