這里看到的這個題
看起來很簡單,就自己寫了一下,然后看原文鏈接還是沒看懂,這里自己寫一下自己的想法励背。
我們看一下這個題:
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 + " count2=" + singleTon.count2);
}
}
乍一看俄精,按照我們的思路應(yīng)該是輸出
count1=1 count2=1
我們輸出一下就會發(fā)現(xiàn)其實是輸出的是:
count1=1 count2=0
其實我們的思路是對的疙教,我們知道為變量賦值是在初始化階段,這個時候其實我們的這些SingleTon類包括count1侯养,count2的內(nèi)存其實都已經(jīng)在準(zhǔn)備階段創(chuàng)建出來了畔况,只是說是默認(rèn)值,真正的賦值是在初始化階段慧库。
只不過我們忽略了一個靜態(tài)變量初始化的順序問題跷跪,JVM加載的時候靜態(tài)變量是按照順序加載的,這個類里面一共有三個靜態(tài)變量齐板,所以這三步是順序執(zhí)行的:
1吵瞻、 private static SingleTon singleTon = new SingleTon();
2葛菇、 public static int count1;
3、 public static int count2 = 0;
第一步橡羞,加載SingleTon眯停,這里調(diào)用了new SingleTon()賦值,也就是會調(diào)用構(gòu)造方法里面的值卿泽。
private SingleTon() {
count1++;
count2++;
}
這里的值如果打印的話會發(fā)現(xiàn)count1莺债,count2由0變成了1。
這個時候1签夭、SingleTon構(gòu)造結(jié)束齐邦。會繼續(xù)往后執(zhí)行2跟3。給count1第租,count2賦值措拇。
第二步,我們看到count1是沒有做賦值操作的慎宾,那么就不需要進(jìn)行賦值丐吓,保持原來的就可以了,這個時候count1的值是1趟据。
第三步券犁,給count2賦值為0
所以,最終結(jié)果是count1=1 count2=0
我們可以驗證一下給這三步換個順序就很明顯了~
這個其實是考驗我們對虛擬機類加載過程的了解之宿,先畫個圖族操。
一、加載
虛擬機規(guī)范要求比被,虛擬在這個階段需要完成以下3件事情:
- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流
- 將這個字節(jié)流所代表的的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象色难,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
加載階段完成后,虛擬機外部的二進(jìn)制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中等缀。也就是ClassLoader起作用的階段枷莉。
二、連接
驗證尺迂、準(zhǔn)備笤妙、解析稱為連接階段。
1噪裕、驗證
驗證是連接階段的第一步蹲盘,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,并且不會危害虛擬機自身的安全膳音。
2召衔、準(zhǔn)備
準(zhǔn)備階段是正是為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存將都在方法區(qū)中進(jìn)行分配祭陷。這里的進(jìn)行內(nèi)存分配的類變量是被static修飾的變量苍凛,而不包括實例變量趣席,實例變量將會在對象實例化時分配在堆中。而final static修飾的會直接賦值為對應(yīng)的值醇蝴。
數(shù)據(jù)類型 | 零值 | 數(shù)據(jù)類型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte)0 |
3宣肚、解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程,比如在方法A中使用方法B悠栓,A () {B () ;}霉涨,這里的B () 就是符號引用,初學(xué)java時我們都是知道這是java的引用闸迷,以為B指向B方法的內(nèi)存地址嵌纲,但是這是不完整的,這里的B只是一個符號引用腥沽,它對于方法的調(diào)用沒有太多的實際意義逮走,可以這么認(rèn)為,他就是給程序員看的一個標(biāo)志今阳,讓程序員知道师溅,這個方法可以這么調(diào)用,但是B方法實際調(diào)用時是通過一個指針指向B方法的內(nèi)存地址盾舌,這個指針才是真正負(fù)責(zé)方法調(diào)用墓臭,他就是直接引用。
三妖谴、初始化
初始化階段是類加載過程的最后一步窿锉,前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外膝舅,其余動作完全由虛擬機主導(dǎo)和控制嗡载,到了初始化階段,才真正開始執(zhí)行勒種定義的Java程序代碼仍稀。
初始化結(jié)束后一個類的加載就結(jié)束了洼滚。這個時候我們的Class類和類變量(static變量)已經(jīng)在方法區(qū)了。
這個時候我們再回過頭來看上面的那個題其實就很清晰了技潘,其實就是一個初始化階段賦值順序的問題遥巴。
class SingleTon {
private static SingleTon singleTon = new SingleTon(); // 2?? 給singleTon分配空間并設(shè)置初始值為null 5??singleTon初始化為 new SingleTon()
public static int count1; // 3?? count1分配空間并設(shè)置初始值為0 7?? count1沒有初始化賦值,故保持當(dāng)前的值享幽,當(dāng)前值為1
public static int count2 = 0; // 4?? count2分配空間并設(shè)置初始值為0 8?? count2 初始化賦值為0
private SingleTon() { // 6?? SingleTon構(gòu)造方法
//當(dāng)前 count1 = 0 count2 = 0
count1++;
count2++;
//++完之后 count1 = 1 count2 = 1
}
public static SingleTon getInstance() {
return singleTon;
}
}
public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance(); // 1?? 加載SingleTon類铲掐; 9?? 調(diào)用getInstance()方法
System.out.println("count1=" + singleTon.count1 + "count2=" + singleTon.count2);
}
}
我們來分析下這里主要涉及的類加載的階段:
一、加載
① 加載SingleTon類
二值桩、連接
1迹炼、驗證
2、準(zhǔn)備
② 給singleTon分配空間并設(shè)置初始值為null
③ count1分配空間并設(shè)置初始值為0
④ count2分配空間并設(shè)置初始值為0
3颠毙、解析
三斯入、初始化
⑤singleTon初始化為 new SingleTon()
⑥ SingleTon構(gòu)造方法 (結(jié)束后 count1 = 1 count2 = 1)
⑦ count1沒有初始化值,故保持當(dāng)前的值蛀蜜,當(dāng)前值為1
⑧ count2 初始化賦值為0
⑨ 調(diào)用getInstance()方法