Java類載入過程&&靜態程式碼塊的初始化過程
問題的引入
還是老規矩,先說說自己遇到的問題。
最近看到了一個比較有意思的Java程式,初次看到這段程式執行的結果還是挺讓我意外的,話不多說先上程式,大家也可以揣摩一下(大神自行略過......)
class Singleton{ private static Singleton singleton=new Singleton(); public static int count1=0; public static int count2; private Singleton(){ count1++; count2++; } public static Singleton getInstance(){ return singleton; } } public class MyTest { public static void main(String[] args) { Singleton instance = Singleton.getInstance(); System.out.println("count1:"+instance.count1); System.out.println("count2:"+instance.count2); } }
看到這裡我想大家已經有了這個程式的結果了把。不知道大家的結果是否正確:
如果你對這個結果很意外那請你接著往下看吧,嘻嘻。如果你答對了,如果你答對了也建議你看完這篇博文,或許你可以收穫一點東西,讓你的思路更加清晰。
知識點回顧
(敲黑板了,敲黑板了)
首先我們需要明確的就是,在Java中靜態變數如果在定義時賦初值實際上就是在在靜態程式碼塊中賦初值,(這一過程我們可以通過反編譯工具檢視細節,這裡不做贅述);同樣的非靜態成員的如果在定義時賦初值,實際上就是在構造器的第一行初始化的改變數。
也就是說上面的程式碼在編譯後,會自動將代程式碼程式設計這樣
class Singleton{ private static Singleton singleton; public static int count1=0; public static int count2; static{ singleton=new Singleton(); count1=0; } private Singleton(){ count1++; count2++; } public static Singleton getInstance(){ return singleton; } } public class MyTest { public static void main(String[] args) { Singleton instance = Singleton.getInstance(); System.out.println("count1:"+instance.count1); System.out.println("count2:"+instance.count2); } }
上面的這個可以通過反編譯工具檢視,但是部分反編譯工具反編譯後的效果仍然是我第一次寫的那樣,但這些都不是重點,我們只需要知道實際上,在定義靜態變數時附的初始值實際上在編譯後會移到靜態程式碼塊中進行,而靜態程式碼塊的作用域構造器的優點相似,都是用於初始化,但是不同的是構造器是用於初始化非靜態變數的,而靜態程式碼塊是用於初始化靜態變數的。
上面就是通過XJad反編譯工具開啟的效果,注意最後的靜態程式碼塊(不同的反編譯工具,反編譯後的程式碼會有差異)
回顧這一個知識點,就是明確類的靜態變數的初始化,是在靜態程式碼塊中進行的。
回到正題
我們都知道,一個類在被首次主動使用之前會被類載入進記憶體並初始化,而在初始化之前,JVM到底做了些什麼呢?這裡我們來簡單的說一下。
類載入的步驟:
第一步:類的載入
第二步:連線
第三步:類的初始化
類的載入
類的載入指的是將.class檔案載入進記憶體
連線
連線就是將已經讀入到記憶體的類的二進位制資料合併到虛擬機器執行時環境中去。
連線也分為三步:
第一步:驗證
確保載入的位元組碼檔案的正確性
第二步:準備
為類的靜態變數分配記憶體,並將初始化預設值。short,int,long的預設值為0,boolean的預設值為false,引用型別的預設 值為null.
第三步:解析
把類中的符號引用轉換為直接引用。
類的初始化
為類的靜態變數賦予正確的初始值,實際上就是執行靜態程式碼塊中的內容。
由上面的的描述我們就知道,一個類載入進記憶體會先為靜態變數分配記憶體,並指定初始值。最後一步才執行初始化。
程式碼分析
我們在執行Singleton instance = Singleton.getInstance();時,由於此時Singleton還沒被載入進虛擬機器,所以虛擬機器會自動的載入它,在連線階段會為singleton,count1,count2分配記憶體,並賦上初始值。在連線階段完成後會進行類的初始化,這一過程實際上就是執行類的靜態程式碼塊,首先會先執行singleton=new Singleton();,執行完畢後,count1和count2都為1。然後執行count1=0,此時count1等於0,count2等於1,這也就是最後輸出的結果。
思考:如果把程式碼改成這樣會輸出什麼?
class Singleton{
public static int count1=0;
private static Singleton singleton=new Singleton();
public static int count2=0;
private Singleton(){
count1++;
count2++;
}
public static Singleton getInstance(){
return singleton;
}
}
public class MyTest {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
System.out.println("count1:"+instance.count1);
System.out.println("count2:"+instance.count2);
}
}
答案:count1=1 count2=0
總結
一個類被載入進記憶體分為類的載入、連線、初始化三個階段。
一個類的靜態變數的初始值是在類載入過程中的連線階段中完成的,而類的初始化過程就是在類的初始化階段中完成的,這個階段中會執行靜態程式碼塊,為靜態變數賦予正確的值。(程式設計師想要的指定的值)