類加載機制與對象初始化
一 . 類加載機制
-
類加載機制是指.class文件加載到jvm並形成Class對象的機制。之后應用可對Class對象進行實例化並調用。類加載機制可在運行時動態加載外部的類,還可以達到類隔離的效果。
-
類從而加載到虛擬機中開始,整個過程分為下圖七個階段,其中驗證,准備,解析統稱為解析。圖中加載,驗證,准備,初始化,卸載這5個階段的順序是確定的,類的加載過程必須按照這種過程按部就班的開始,而解析則不一定,它在某些情況下可能在初始化之后才開始,這是為了支持java語言的運行時綁定。
1 . 裝載: 裝載過程負責找到二進制字節碼並加載至JVM--即負責查找和導入class文件。
2 . 鏈接:
(1) 檢驗:鏈接過程負責對二進制字節碼的格式進行檢驗,檢查class文件數據的正確性 (2) 准備:初始化裝載類中的靜態變量,給類的靜態變量分配存儲空間 (3) 解析:解析類中調用的接口,類,將符號引用轉為直接引用。
3 . 初始化:初始化過程即執行類中的靜態初始化代碼,構造器代碼以及靜態屬性的初始化。以下四種情況會立即觸發初始化
(1) 調用了new,讀取或設置一個類的靜態字段的時候,或者調用一個類的靜態方法的時候。
(2) 反射調用了類中的方法
(3) 子類調用了初始化,如果子類初始化的時候發現父類尚未初始化,則會先出發父類的初始化。
(4) JVM啟動過程中指定的初始化類(包含main方法的類)
(5)當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有初始化,則需要先觸發其初始化.
-
JVM 的類加載通過ClassLoader及其子類來完成,分別是Bootstrap ClassLoader,Extension ClassLoader,App ClassLoader 以及用戶自定義的繼承自ClassLoader抽象類的實現Custom ClassLoader。
-
(1) Bootstrap ClassLoader : 此類並非ClassLoader的子類,在代碼中沒辦法拿到這個類的對象,Sun JDK會在啟動時自動加載此類。此加載器會將存放於<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 rt.jar 名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中
(2) Extension ClassLoader : 此加載器會將<JAVA_HOME>\lib\ext目錄下的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫加載.
(3) App ClassLoader : 此加載器會將用戶類路徑(ClassPath)上所指定的類庫加載。
-
(4) Custom ClassLoader : 基於自定義的ClassLoader加載非classpath中的jar及目錄,還可以在加載之前對class文件做一些動作,例如加密。
- JVM 的ClassLoader采用的是樹形結構,除 Bootstrap ClassLoader外都會有父級的ClassLoader。加載類時通常會按照樹形結構的原則進行。如果一個類加載器接收到了類加載的請求,它首先把這個請求委托給他的父類加載器去完成,每個層次的類加載器都是如此,因此所有的加載請求都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它在搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。 這樣做的好處是:java類隨着它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都會委派給啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果用戶自己寫了一個名為java.lang.Object的類,並放在程序的Classpath中,那系統中將會出現多個不同的Object類,java類型體系中最基礎的行為也無法保證,應用程序也會變得一片混亂。
二 . 初始化
java會盡量保證變量在使用前得到恰當的初始化,對於局部變量,如果未初始化會得到編譯時的錯誤,對於成員變量會自動初始化(0,null等)。
可以使用構造方法初始化成員變量,在運行時刻,可以調用方法或者執行某些動作來確定初值。但需要記住一點:無法阻止自動初始化的進行,它在構造方法被調用之前發生。對於所有基本類型和對象引用,包括在定義時已經指定初始值的變量,情況也是一樣的.例如如下實例代碼,i首先會被置為0,然后替換為指定的初始值7.
public class TestClass(){ int i;//自動初始化賦值為0 public static void main(String[] args){ i = 7;//賦值為7 } }
1 . 初始化的順序:在類的內部,變量定義的先后順序決定了初始化的順序。即使變量的定義散布於方法定義之間,他們仍會在任何方法(包括構造方法)被調用之前得到初始化
對於如下代碼:w3這個引用會被初始化兩次,一次在調用構造方法之前,一次在調用期間,這時第一次引用的對象將被丟棄並被垃圾回收.
public class House { Window w1 = new Window(1); public House() { System.out.println("House()"); w3 = new Window(3_3); } Window w2 = new Window(2); public void method() { System.out.println("method()"); } Window w3 = new Window(3); public static void main(String[] args) { House house = new House(); } } class Window { Window(int order) { System.out.println("window" + order); } } //輸出結果為: window1 window2 window3 House() window33
2 . 靜態數據的初始化
靜態初始化只有在必要的時候才進行,初始化的順序是先靜態對象(如果它們尚未因前面的的對象創建過程而被初始化),而后是“非靜態”對象。
示例代碼如下:
public class StaticInitailization { public static void main(String[] args) { System.out.println("creating new CupBoard() in main"); new CupBoard(); System.out.println("creating new CupBoard() in main"); new CupBoard(); table.f2(1); cupBoard.f3(1); } static Table table = new Table(); static CupBoard cupBoard = new CupBoard(); } class Bowl { public Bowl(int i) { System.out.println("Bowl(" + i + ")"); } void f1(int i) { System.out.println("f1(" + i + ")"); } } class Table { static Bowl bowl1 = new Bowl(1); Table() { System.out.println("Table()"); bowl2.f1(1); } void f2(int i) { System.out.println("f2(" + i + ")"); } static Bowl bowl2 = new Bowl(2); } class CupBoard { Bowl bowl3 = new Bowl(3); static Bowl bowl4 = new Bowl(4); CupBoard() { System.out.println("CupBoard()"); bowl4.f1(2); } void f3(int i) { System.out.println("f3(" + i + ")"); } static Bowl bowl5 = new Bowl(5); } // 結果如下: Bowl(1) Bowl(2) Table() f1(1) Bowl(4) Bowl(5) Bowl(3) CupBoard() f1(2) creating new CupBoard() in main Bowl(3) CupBoard() f1(2) creating new CupBoard() in main Bowl(3) CupBoard() f1(2) f2(1) f3(1)
3 . 對象的創建過程: 假設有個名為Dog 的類
(1) 即使沒有顯示的使用static關鍵字,構造方法實際上也是靜態方法。因此,當首次創建Dog類的對象時,或者Dog類的靜態方法或者靜態變量被訪問時,java解釋器必須先查找類路徑,以定位Dog.class文件
(2)載入Dog.class文件(這將創建一個Class對象),有關靜態初始化的所有動作都會執行。因此,靜態初始化只在類首次加載的時候進行一次
(3)用new Dog 創建對象的時候,首先會在堆上為該對象分配足夠的存儲空間
(4)這塊存儲空間會被清零,這將會自動的為該對象的的所有基本數據類型賦初值
(5)執行所有出現於字段定義處的初始化動作。
(6)執行構造方法。
-
復雜對象調用構造方法的順序:
(1) 在其他任何事物發生之前,將分配給對象的的存儲空間初始化為二進制的零
(2) 調用父類的構造方法
(3) 按照聲明順序調用成員的初始化方法
(4) 調用子類構造方法的主體部分 -
編寫構方法注意避免調用其他方法,在構造方法內唯一能安全調用的方法是父類中的final方法(也適用於private方法,他們自動屬於final方法)。這些方法不能被override(覆寫)
參考書籍:分布式java應用基礎與實踐--林昊 3.1.2節 類加載機制
參考博客1:深入理解Java:類加載機制及反射
參考博客2:Java中普通代碼塊,構造代碼塊,靜態代碼塊區別及代碼示例