網上介紹Java類加載的文章不計其數,但大多都千篇一律。之前有打算寫一下類加載,一直感覺自己理解不是很透徹,現在感覺可以出鍋了,哦,不對,可以出徒了,也不對,可以寫博了。廢話不多說,上乾貨。
不加例子的解釋都是耍流氓,先來個簡單的例子:
public class TestStatic {
private static TestStatic tester = new TestStatic();
private static int count1;
private static int count2 = 2;
public TestStatic() {
count1++;
count2++;
System.out.println("" + count1 + "\t" + count2);
}
static {
System.out.println("靜態代碼塊執行:" + count1);
}
{
System.out.println("構造代碼塊執行");
}
public static TestStatic getTester() {
System.out.println(count1 + "\t" + count2);
return tester;
}
public static void main(String[] args) {
System.out.println(TestStatic.getTester());
}
}
這段代碼的執行結果是:
構造代碼塊執行
1 1
靜態代碼塊執行:1
1 2
如果答對的同學,可以下樹了,答錯的同學,聽我娓娓道來。
- 首先經過類加載器加載,
TestStatic
對象的二進制字節流已進入內存。 TestStatic
被jvm標記爲啓動類了(包含main函數),觸發類加載, 在鏈接階段的準備階段,變量tester
的初始值爲null,count1
爲0,count2
也爲0,- 鏈接準備階段的賦值操作完成,觸發類加載的初始化階段。首先觸發類構造器執行,執行到
TestStatic tester = new TestStatic();
,實例構造器開始執行。故構造代碼塊首先執行,打印出:構造代碼塊執行。然後執行構造函數,count1和count2此時都是鏈接準備階段的賦值結果0,然後自增,故打印出1,1。tester賦值完畢,然後繼續執行,count1沒有重新賦值,還是1,count2被重新賦值,變成了2。然後執行靜態代碼塊,故打印出靜態代碼塊執行:1。至此,類加載完成。 - 最後調用類的靜態方法,會觸發類加載,因爲此時
TestStatic
已經被jvm加載過,故不在重新加載,直接調用方法getTester
,打印出 1 2
何時觸發類的加載
主動引用的類會觸發類加載機制,被動引用則不會觸發。
主動引用
- 調用類的靜態屬性
- 調用類的靜態方法
- 使用反射加載類
Class.forName("com.demo.Student")
- 使用
new
關鍵字創建對象。 - 被jvm標記爲啓動類
main
函數所在的類。 - 初始化子類,若父類未被初始化,則首先初始化父類。
被動引用
- 調用父類的靜態變量,不會觸發子類加載,只會加載父類
- 通過一個類創建數組引用,
Student[] students = new Student[]{10};
- 調用類的常量
類是怎樣被加載
.java源文件被編譯成.class文件,通過Java的類加載器,把.class文件加載到jvm的方法區中,在堆中生成代表次類的Class對象,用來封裝方法區的數據結構。一個類無論產生多少對象,其Class對象只有這一個。
類加載器
- 系統類加載器(
Bootstrap ClassLoader
)
Java最底層的類加載器,負責加載$JAVA_HOME/jre/lib/rt.jar
中的class,String
,Object
,Integer
等就是由該類加載器加載,由C++實現。 - 擴展類加載器(
Extension ClassLoade
)
加載Java平臺擴展的jar包,負責加載$JAVA_HOME/jre/lib/ext/*.jar
中的class,或者-Djava.etx.dirs
指定jar包中的類。 - 應用類加載器(
App Classloader
)
最常用的類加載器,classpath
中的類及目錄中的class。 - 自定義類加載器
根據需要,用戶自定義的類加載器,按照jvm規範。tomcat,jboss都根據規範實現了自己的classloader
雙親委派模型
如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器纔會嘗試自己去加載,這就是雙親委派模式。
雙親委派模型保證了Java的安全性,試想,如果你自定義了一個java.lang.Object,通過雙親委派模型,最終會讓系統類加載器加載,你自定義的類加載器不會被加載。保證了Object的安全性。另外有些j2ee服務器需要打破這種雙親委派模型。
類加載的三個步驟
1:加載
查找並加載類的二進制數據、將硬盤上的.class文件加載進內存中。
2:鏈接
鏈接又分爲三個步驟:
- 驗證
確保被加載類的正確性,也就是javac編譯的class文件的正確性 - 準備(重要)
爲類的靜態屬性賦初始值,int 初始值爲0,boolean初始值爲false,引用類型初始值爲null。特別注意:如果類變量被final
修飾,此時直接賦值爲程序猿聲明的值。 - 解析
將類的符號引用轉化爲直接引用,也就是將對象的引用轉化爲指針。
3:初始化
常規情況:
- 首先初始化類構造器,編譯器自動收集類中的靜態變量,靜態代碼塊合併產生類構造器,順序按照代碼中出現的順序
- 然後初始化實例構造器,編譯器自動收集類中 實例變量賦值動作,實例代碼塊,構造函數,合併成實例構造器。
實力初始化並不一定在類構造器之後執行,有可能類初始化包含實例初始化,這句話非常重要,好好體會,便能明白上面的題的原理