下邊是一道筆試題:
class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
}
public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
我想大部分人的第一答案都是:
count1=1
count2=1
其實它的正確答案是:
count1=1
count2=0
爲什麼會這樣呢?認真看完下邊的講解,就會明瞭。
一、類加載時機
類從被加載到虛擬機內存中開始,直到卸載出內存爲止,它的整個生命週期包括了:加載、驗證、準備、解析、初始化、使用和卸載這7個階段。其中,驗證、準備和解析這三個部分統稱爲連接(linking)。
其中,加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類的加載過程必須按照這種順序按部就班的“開始”(僅僅指的是開始,而非執行或者結束,因爲這些階段通常都是互相交叉的混合進行,通常會在一個階段執行的過程中調用或者激活另一個階段),而解析階段則不一定(它在某些情況下可以在初始化階段之後再開始,這是爲了支持Java語言的運行時綁定。
二、什麼時候開始初始化
- 創建類的實例的時候;
- 訪問類的靜態變量(除常量【被final修飾的靜態變量】原因:常量一種特殊的變量,因爲編譯器把他們當作值(value)而不是域(field)來對待。如果你的代碼中用到了常變量(constant variable),編譯器並不會生成字節碼來從對象中載入域的值,而是直接把這個值插入到字節碼中。這是一種很有用的優化,但是如果你需要改變final域的值那麼每一塊用到那個域的代碼都需要重新編譯;
- 訪問類的靜態變量;
- 反射,如(Class.forName(“my.xyz.Test”));
- 當初始化一個類時,發現其父類還未初始化,則先觸發父類的初始化;
- 虛擬機啓動時,定義了main()方法的那個類先初始化
三、類加載過程
- 加載, 在加載階段虛擬機需要完成三件事
- 通過一個類的全限定名來獲取定義此類的二進制字節流
- 將字節流所代表的的靜態存儲結構轉化爲方法區的運行時數據結構
- 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口
- 驗證, 驗證就是確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全(java文件本身是相對安全的,但是改爲Class文件後可能危害到虛擬機或程序的安全)
- 文件格式驗證:驗證字節流是否符合Class文件格式的規範
- 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求
- 字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的
- 符號引用驗證:確保解析動作能正確執行。
- 準備, 準備階段是正式爲類靜態變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都在方法區中進行分配
- 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中
- 這裏所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值
public static int value = 123 //在準備階段 value的值是 0 並不是123
如果屬性有Constant Value 屬性( 被static和final修飾),那麼在準備階段變量就會被初始化爲所指定的值
public static final int value = 123 // 準備階段value 的值爲123
- 解析, 解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程
- 初始化, 爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。
在Java中對類變量進行初始值設定有兩種方式:
①聲明類變量是指定初始值
②使用靜態代碼塊爲類變量指定初始值
JVM初始化步驟:
1、假如這個類還沒有被加載和連接,則程序先加載並連接該類
2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3、假如類中有初始化語句,則系統依次執行這些初始化語句
看到這裏,我們再回頭分析上邊的例子:
1、SingleTon singleTon = SingleTon.getInstance();調用了類的SingleTon調用了類的靜態方法,觸發類的初始化
2、類加載的時候在準備過程中爲類的靜態變量分配內存並初始化默認值 singleton=null count1=0,count2=0
3、類初始化,爲類的靜態變量賦值和執行靜態代碼快。singleton賦值爲new SingleTon()調用類的構造方法
4、調用類的構造方法後count=1;count2=1
5、繼續爲count1與count2賦值,此時count1沒有賦值操作,所有count1爲1,但是count2執行賦值操作就變爲0
四、類加載器
把類加載階段的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作交給虛擬機之外的類加載器來完成。這樣的好處在於,我們可以自行實現類加載器來加載其他格式的類,只要是二進制字節流就行,這就大大增強了加載器靈活性。
- 啓動類加載器: 負責加載%JAVA_HOME%\bin目錄下的所有jar包,或者是-Xbootclasspath參數指定的路徑;
- 擴展類加載器: 負責加載%JAVA_HOME%\bin\ext目錄下的所有jar包,或者是java.ext.dirs參數指定的路徑;
- 應用程序類加載器: 負責加載用戶類路徑上所指定的類庫,如果應用程序中沒有自定義加載器,那麼此加載器就爲默認加載器。
那麼問題來了,如果同時存在兩個或多個全限定名完全一樣的情況,該如何選擇類呢,這就是雙親委派機制要做的工作。
雙親委派機制的原理:
1-類加載器收到類加載的請求;
2-把這個請求委託給父加載器去完成,一直向上委託,直到啓動類加載器;
3-啓動器加載器檢查能不能加載(使用findClass()方法),能就加載(結束);否則,拋出異常,通知子加載器進行加載。
4- 只有當 父類加載器 反饋 自己無法完成該加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會自己去加載;