我們先看兩段代碼的運行結果
public class Test1 {
public static void main(String[] args) {
System.out.println(FinalTest.NUM);
System.out.println(FinalTest1.NUM);
}
}
class FinalTest{
public static final int NUM = 3;
static{
System.out.println("hello");
}
}
class FinalTest1{
public static int NUM = 5;
static{
System.out.println("hello1");
}
}
打印結果:
3
hello1
5
問題1:為什么"hello" 沒有被打印出來?
public class Singleton {
public static Singleton instance = new Singleton();
public static int a1;
public static int a2 = 0;
public Singleton() {
a1++;
a2++;
}
public static Singleton getSingleton() {
return instance;
}
}
public class TestSingleton {
public static void main(String[] args) {
Singleton singleton = Singleton.getSingleton();
System.out.print(singleton.a1);
System.out.print(singleton.a2);
}
}
打印結果: 10
問題2:為什么結果不是 11 而是 10? a2為什么是0?
問題1涉及到Java的類加載條件等,問題2涉及到Java類加載步驟,上述兩個問題的答案看完下文便知.
什么是Java的類加載
我們都知道,Java類編譯完會成為.class文件.
類的加載指的是將類的.class文件中的二進制數(shù)據(jù)讀入內(nèi)存中,將其放在運行時數(shù)據(jù)區(qū)域的方法去內(nèi),然后在堆中創(chuàng)建java.lang.Class對象,用來封裝類在方法區(qū)的數(shù)據(jù)結構.只有java虛擬機才會創(chuàng)建class對象,并且是一一對應關系.這樣才能通過反射找到相應的類信息.
誰來加載
Java字節(jié)碼的加載是由類加載器(ClassLoader)加載的.
至于ClassLoader的雙親委托模式,其實就是一個遞歸. 其流程如下
當類加載器收到加載類或資源的請求時龙亲,通常都是先委托給父類加載器加載犹菱,也就是說只有當父類加載器找不到指定類或資源時唉侄,自身才會執(zhí)行實際的類加載過程着绷,具體的加載過程如下:
源 ClassLoader 先判斷該 Class 是否已加載图呢,如果已加載褐桌,則直接返回 Class照雁,如果沒有則委托給父類加載器秋泄。
父類加載器判斷是否加載過該 Class阱持,如果已加載夭拌,則直接返回 Class,如果沒有則委托給祖父類加載器衷咽。
依此類推叁丧,直到始祖類加載器(引用類加載器)疹娶。
始祖類加載器判斷是否加載過該 Class,如果已加載,則直接返回 Class蚕冬,如果沒有則嘗試從其對應的類路徑下尋找 class 字節(jié)碼文件并載入骄崩。如果載入成功映企,則直接返回 Class墨榄,如果載入失敗,則委托給始祖類加載器的子類加載器此蜈。
始祖類加載器的子類加載器嘗試從其對應的類路徑下尋找 class 字節(jié)碼文件并載入即横。如果載入成功,則直接返回 Class裆赵,如果載入失敗东囚,則委托給始祖類加載器的孫類加載器。
依此類推战授,直到源 ClassLoader页藻。
源 ClassLoader 嘗試從其對應的類路徑下尋找 class 字節(jié)碼文件并載入。如果載入成功植兰,則直接返回 Class份帐,如果載入失敗,源 ClassLoader 不會再委托其子類加載器楣导,而是拋出異常废境。
了解更多,可以看關于ClassLoader那點事
類加載條件 (問題1答案)
那么我們什么時候類需要加載呢?
當我們首次主動使用這個類的時候,類需要被加載.
那什么是“主動使用”呢?
- 創(chuàng)建對象的實例:我們new對象的時候筒繁,會引發(fā)類的初始化噩凹,前提是這個類沒有被初始化。
- 調(diào)用類的靜態(tài)屬性或者為靜態(tài)屬性賦值
- 調(diào)用類的靜態(tài)方法
- 通過class文件反射創(chuàng)建對象
- 初始化一個類的子類:使用子類的時候先初始化父類
- java虛擬機啟動時被標記為啟動類的類:就是我們的main方法所在的類
只有上面6種情況才是主動使用毡咏,也只有上面六種情況的發(fā)生才會引發(fā)類的初始化。
同時我們需要注意下面幾個Tips:
- 在同一個類加載器下面只能初始化類一次,如果已經(jīng)初始化了就不必要初始化了.
這里多說一點血当,為什么只初始化一次呢?因為我們上面講到過類加載的最終結果就是在堆中存有唯一一個Class對象臊旭,我們通過Class對象找到
類的相關信息落恼。唯一一個Class對象說明了類只需要初始化一次即可离熏,如果再次初始化就會出現(xiàn)多個Class對象,這樣和唯一相違背了滋戳。 - 在編譯的時候能確定下來的靜態(tài)變量(編譯常量),不會對類進行初始化;
- 在編譯時無法確定下來的靜態(tài)變量(運行時常量),會對類進行初始化;
- 如果這個類沒有被加載和連接的話,那就需要進行加載和連接
- 如果這個類有父類并且這個父類沒有被初始化,則先初始化父類.
- 如果類中存在初始化語句,依次執(zhí)行初始化語句.
這個時候問題1的答案我們可以知道了, 其實FinalTest
的靜態(tài)代碼塊沒被執(zhí)行的原因是這個類沒有被加載,因為我們的NUM
是final的,屬于編譯常量,在編譯時就確定下來的靜態(tài)變量,不會對類進行初始化.
從字節(jié)碼到內(nèi)存中的對象步驟
從字節(jié)碼到內(nèi)存中的對象, 期間要經(jīng)過三大步, 為 裝載(Loading)、鏈接(Linking)和初始化(Initialization)
加載(Loading)
按如下三步執(zhí)行
- 通過類的全名產(chǎn)生對應類的二進制數(shù)據(jù)流奸鸯。(注意咪笑,如果沒找到對應類文件,只有在類實際使用時才拋出錯誤娄涩。)
- 分析并將這些二進制數(shù)據(jù)流轉換為方法區(qū)(JVM 的架構:方法區(qū)窗怒、堆,棧蓄拣,本地方法棧扬虚,pc 寄存器)特定的數(shù)據(jù)結構(這些數(shù)據(jù)結構是實現(xiàn)有關的,不同 JVM 有不同實現(xiàn))球恤。這里處理了部分檢驗辜昵,比如類文件的魔數(shù)的驗證,檢查文件是否過長或者過短咽斧,確定是否有父類(除了 Obecjt 類)堪置。
- 創(chuàng)建對應類的 java.lang.Class 實例(注意,有了對應的 Class 實例张惹,并不意味著這個類已經(jīng)完成了加載鏈鏈接=)。
鏈接(Linking)
類的連接有三步诵叁,分別是驗證雁竞,準備,解析拧额。
驗證
驗證階段主要做了以下工作
- 將已經(jīng)讀入到內(nèi)存類的二進制數(shù)據(jù)合并到虛擬機運行時環(huán)境中去碑诉。
- 類文件結構檢查:格式符合jvm規(guī)范-語義檢查:符合java語言規(guī)范,final類沒有子類,final類型方法沒有被覆蓋
- 字節(jié)碼驗證:確保字節(jié)碼可以安全的被java虛擬機執(zhí)行.
二進制兼容性檢查:確保互相引用的類的一致性.如A類的a方法會調(diào)用B類的b方法.那么java虛擬機在驗證A類的時候會檢查B類的b方法是否存在并檢查版本兼容性.因為有可能A類是由jdk1.7編譯的侥锦,而B類是由1.8編譯的进栽。那根據(jù)向下兼容的性質,A類引用B類可能會出錯恭垦,注意是可能快毛。
準備階段
java虛擬機為類的靜態(tài)變量分配內(nèi)存并賦予默認的初始值.如int分配4個字節(jié)并賦值為0,long分配8字節(jié)并賦值為0;
解析階段
解析階段主要是將符號引用轉化為直接引用的過程格嗅。比如 A類中的a方法引用了B類中的b方法,那么它會找到B類的b方法的內(nèi)存地址唠帝,將符號引用替換為直接引用(內(nèi)存地址)屯掖。
初始化步驟(問題2答案)
一種是類有父類,一種是類沒有父類襟衰。(當然所有類的頂級父類都是Object)
沒有父類的情況:
類的靜態(tài)屬性
類的靜態(tài)代碼塊
類的非靜態(tài)屬性
類的非靜態(tài)代碼塊
構造方法有父類的情況:
父類的靜態(tài)屬性
父類的靜態(tài)代碼塊
子類的靜態(tài)屬性
子類的靜態(tài)代碼塊
父類的非靜態(tài)屬性
父類的非靜態(tài)代碼塊
父類構造方法
子類非靜態(tài)屬性
子類非靜態(tài)代碼塊
子類構造方法
在這要說明下贴铜,靜態(tài)代碼塊和靜態(tài)屬性是等價的,他們是按照代碼順序執(zhí)行的瀑晒。
那么問題2的答案來了. 我們再回到問題2的代碼里面.
第一步,我們是調(diào)用了Singleton
的靜態(tài)方法. 這個時候觸發(fā)了這個類是要被加載了.
首先,讀取磁盤上的class文件,各種驗證完畢,進行鏈接,準備階段給這個類的三個變量都進行初始化.
此時:
instance = null;
a1 = 0;
a2 = 0;
之后我們要進行初始化了,這個類么有父類.那么我們進行初始化類的靜態(tài)屬性.
第一步:
instance = new Singleton();
這個時候new Singleton()
要走構造方法了.此時a1++ 之后 a1 = 1; a2 = 1;
按照順序執(zhí)行,第二步給a1賦值為1
第三部給a2賦值為0
之后我們沒有走其他步驟, 隨后打印a1與a2的值,也就是10了.
本文作者:Anderson/Jerey_Jobs
博客地址 : http://jerey.cn/
簡書地址 : Anderson大碼渣
github地址 : https://github.com/Jerey-Jobs