讀Java虛擬機類加載引起的血案

/ 前言 /

最近在看 Java 虛擬機類加載的知識點,結果讓我發現了本身一個曾經一直糾結,又沒完全弄懂的類加載黑洞,從而引起下面一系列的測試血案。java

相信面試過的大家也會見過相似下面測試的這幾道題。不過,答案你真的理解了麼?話很少說,直接 GKD。惋惜我不是大佬,因此...哈哈哈 GKD 吧!下面就是測試過程種發現的一些疑惑點,趕忙記錄一波...面試

/ 正文 /

測試開始,先思考下下面代碼輸出什麼:數組

class Singleton {

    public Singleton() {
        System.out.println("Singleton new instance");
    }

    static {
        System.out.println("Singleton static block");
    }

    {
        System.out.println("Singleton  block !!!");
    }

}

public class NewTest {
    public static void main(String args[]){
        Singleton singleton = new Singleton();
    }
}

輸出結果:架構

Singleton static block
Singleton  block !!!
Singleton new instance

固然,大佬們應該都能知道答案...畢竟,新手入門級的野怪,誰都打得過。這個對我這小菜雞也算還比較容易理解;加載鏈接過程,沒有須要處理的 static。new Singleton() 直接開始類的初始化了,因此輸出直接按照類的初始化順序來就行了dom

類的初始化的執行順序

沒有父類的狀況:函數

類的靜態屬性
類的靜態代碼塊
類的非靜態屬性
類的非靜態代碼塊
構造方法學習

有父類的狀況:測試

父類的靜態屬性
父類的靜態代碼塊
子類的靜態屬性
子類的靜態代碼塊
父類的非靜態屬性
父類的非靜態代碼塊
父類構造方法
子類非靜態屬性
子類非靜態代碼塊
子類構造方法優化

這裏有個小誤區,是我本身的誤區~~好比下面這個例子:code

class ParentSingleton{

public static int value = 100;

public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}

static {
    System.out.println("ParentSingleton static block");
}

{
    System.out.println("ParentSingleton  block !!! ");
}

}

當要初始化上面這個類的時候,會輸出什麼?

若是這時候,咱們只看上面的初始化順序,會以爲這樣輸出,根據順序來嘛~

ParentSingleton static block
ParentSingleton  block !!!
ParentSingleton new instance
???

OMG,錯了,這裏的順序不是說,只要初始化,就要所有按照順序一一執行...不是這樣的。實際上只會輸出:

ParentSingleton static block

若是有建立這個類的實例,好比 new ParentSingleton(),纔會:

ParentSingleton block !!!
ParentSingleton new instance

是的,這裏的誤區,我曾經一度搞錯了...尷尬。那再看這個測試:

class Singleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static void forTest() {

}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton.forTest();
}
}

看完資料的我,逐漸膨脹,畢竟100多斤的胖子,我想的輸出應該是:

Singleton static block
Singleton  block !!!
Singleton new instance

而後運行一看,懵逼了,結果是:

Singleton  block !!! 
Singleton new instance
Singleton static block

咋回事啊,小老弟,結果亂套了...爲何不是先執行 static 代碼塊先了。認真想了一波,也不知道對不對,只能瘋狂測試這樣子...

通過一番測試,查看資料...最終...我以爲是這樣子的。整個的流程詳解應該是執行的第一步:Singleton.forTest();這時候,對Singleton類進行加載和鏈接,因此首先須要對它進行加載和鏈接操做。在鏈接-準備階段,要講給靜態變量賦予默認初始值,這裏還沒到執行 forTest;初始值是 singleton = null。加載和鏈接完畢以後,再進行初始化工做:

private static Singleton singleton = new Singleton();

因此執行去到了 new Singleton(); 這裏由於 new 會引發 Singleton 的初始化。須要執行 Singleton構造函數裏面的內容。可是又由於非static初始化塊,這裏面的代碼在建立java對象實例時執行,並且在構造器以前!!!!就是這東西...因此輸出應該是:

Singleton  block !!! 
Singleton new instance

而根據類的初始化順序,要執行 static 代碼塊,應該輸出:

Singleton static block

完成初始化後。接下來就到真正調用 forTest 方法了,方法什麼都不作,沒輸出。因此,總的答案就是:

Singleton  block !!! 
Singleton new instance
Singleton static block

這裏最大的緣由就是,鏈接加載的時候,要給屬性初始化,而這裏的初始化又恰好是 建立java 實例,須要執行構造,執行構造的前面又必須先執行 {} 大括號非 static 塊。而不是和第一個測試例子那樣,static 屬性不須要初始化,因此....

IG 永不加班,但我須要哇,繼續測試吧...繼續測試驗證:

class Singleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return new Singleton();
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = Singleton.getSingleton();
}
}

輸出結果以下所示。emm, 再次根據上面本身的理解,走一遍,應該是:

Singleton  block !!! 
Singleton new instance
Singleton static block
Singleton  block !!! 
Singleton new instance

這裏後面第二次 new 爲啥不引發第二次 類的初始化?? 由於一個類只能初始化一次啊!new 只是建立實例,再也不初始化了。因此在調用 getSingleton 的時候,只建立實例就行了,而建立實例就是:

Singleton  block !!! 
Singleton new instance

在同一個類加載器下面只能初始化類一次,若是已經初始化了就沒必要要初始化了。爲何只初始化一次呢?類加載的最終結果就是在堆中存有惟一一個Class對象,咱們經過Class對象找到的那個惟一的。噢?運行看一手,丟,對了..還有存在 final 的時候,和存在父類的時候,下面慢慢再測試驗證....繼續測試:

class Singleton extends ParentSingleton {

public Singleton() {
    System.out.println("Singleton new instance");
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{

public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}

static {
    System.out.println("ParentSingleton static block");
}

{
    System.out.println("ParentSingleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = new Singleton();
}
}

輸出結果以下所示。這個,很明瞭,仍是按照上面的類的初始化,有父類的狀況按順序調用,輸出以下:

ParentSingleton static block
Singleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance

繼續測試以下所示。那我的,又來了...改爲和上面沒有父類同樣的狀況:

class Singleton extends ParentSingleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return singleton;
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{

public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}

static {
    System.out.println("ParentSingleton static block");
}

{
    System.out.println("ParentSingleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = Singleton.getSingleton();
}
}

輸出結果以下所示。這裏,就開始懵了...有點。先看結果:

ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block

其實,很容易看清了,如今,再走一遍流程吧!執行到 Singleton.getSingleton() 時,先加載 Singleton ,這時由於 Singleton 有父類,須要須要加載父類先,加載父類 ParentSingleton,根據加載流程,在鏈接-準備階段,要講給靜態變量賦予默認初始值,但父類沒有 static 屬性須要賦值初始化什麼的,可是根據順序,須要初始化static 代碼塊:

ParentSingleton static block

這時候回到子類的加載流程。根據鏈接-準備階段,子類有須要處理的屬性 private static Singleton singleton = new Singleton();賦值默認值先,singleton = null;而後初始化 singleton = new Singleton();根據上面的經驗,這裏是建立實例 ,並引發初始化,正常應該是:

Singleton  block !!! 
Singleton new instance
Singleton static block

可是,重點來了 !! 類實例建立過程:按照父子繼承關係進行初始化,首先執行父類的初始化塊部分。而後是父類的構造方法;再執行本類繼承的子類的初始化塊,最後是子類的構造方法,也就是:

ParentSingleton  block !!! 
ParentSingleton new instance

同時子類的初始化,由於初始化子類它有父類,因此須要先初始化父類(可是這裏由於父類已經初始化了,就再也不初始化了)。因此結果是:

ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block

最終測試以下所示:

class Singleton extends ParentSingleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return singleton;
}

static {
    System.out.println("Singleton static block");
}

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{

private static ParentSingleton parentSingleton = new ParentSingleton();   

public ParentSingleton(){

    System.out.println("ParentSingleton new instance");
}

static {
    System.out.println("ParentSingleton static block");
}

{
    System.out.println("ParentSingleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = Singleton.getSingleton();
}
}

測試結果以下所示:

ParentSingleton  block !!! 
ParentSingleton new instance
ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block

加載一個類時,先加載父類。按照先加載,建立實例,初始化,這個順序就發現很通順的寫出答案了。哈哈哈哈哈,終於清楚了。因此一切的一切,都是建立實例這個東西。搞得我頭暈。

部分特殊不引發類初始化記錄,先記錄下吧。

經過子類引用父類的靜態字段,不會致使子類初始化,對於靜態字段,只有直接定義這個字段的類纔會被初始化
經過數組定義來引用類,不會觸發此類的初始化
常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量的類的初始化
public static final int x =6/3; 可以在編譯時期肯定的,叫作編譯常量,不會引發類的初始化!!!
public static final int x =new Random().nextInt(100); 運行時才能肯定下來的,叫作運行時常量,運行常量會引發類的初始化!!!

在虛擬機規範中使用了一個很強烈的限定語:「有且僅有」,這5種場景中的行爲稱爲對類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。

5種必須初始化的場景以下

  1. 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有初始化,則須要先觸發其初始化

這4條指令對應的的常見場景分別是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。

注:靜態內容是跟類關聯的而不是類的對象。

  1. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。

注:反射機制是在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法

對於任意一個對象,都可以調用它的任意一個方法和屬性

這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制,這相對好理解爲何須要初始化類。

  1. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。

注:子類執行構造函數前需先執行父類構造函數

  1. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

注:main方法是程序的執行入口

  1. 當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化。則須要先觸發其初始化。

注:JDK1.7的一種新增的反射機制,都是對類的一種動態操做

這回,之後看代碼的時候,就不會再被這些執行加載順序弄混了,對優化代碼可能仍是有幫助的吧。

再不說,也能再讓我看到這些測試題,或者問我加載的過程,怎麼也能處理回答個7788了吧。

可能其中我的理解有部分紕漏,還請大佬們指出~~蟹蟹鴨!

Android開發資料+面試架構資料 免費分享 點擊連接 便可領取

《Android架構師必備學習資源免費領取(架構視頻+面試專題文檔+學習筆記)》

相關文章
相關標籤/搜索