一個(gè)面試題引出來(lái)的class加載時(shí)機(jī)

這里看到的這個(gè)題
看起來(lái)很簡(jiǎn)單,就自己寫了一下,然后看原文鏈接還是沒看懂,這里自己寫一下自己的想法。
我們看一下這個(gè)題:

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

我們輸出一下就會(huì)發(fā)現(xiàn)其實(shí)是輸出的是:

count1=1 count2=0

其實(shí)我們的思路是對(duì)的,我們知道為變量賦值是在初始化階段,這個(gè)時(shí)候其實(shí)我們的這些SingleTon類包括count1,count2的內(nèi)存其實(shí)都已經(jīng)在準(zhǔn)備階段創(chuàng)建出來(lái)了,只是說(shuō)是默認(rèn)值,真正的賦值是在初始化階段。
只不過(guò)我們忽略了一個(gè)靜態(tài)變量初始化的順序問(wèn)題,JVM加載的時(shí)候靜態(tài)變量是按照順序加載的,這個(gè)類里面一共有三個(gè)靜態(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()賦值,也就是會(huì)調(diào)用構(gòu)造方法里面的值。

private SingleTon() {
        count1++;
        count2++;
    }

這里的值如果打印的話會(huì)發(fā)現(xiàn)count1,count2由0變成了1。
這個(gè)時(shí)候1、SingleTon構(gòu)造結(jié)束。會(huì)繼續(xù)往后執(zhí)行2跟3。給count1,count2賦值。
第二步,我們看到count1是沒有做賦值操作的,那么就不需要進(jìn)行賦值,保持原來(lái)的就可以了,這個(gè)時(shí)候count1的值是1。
第三步,給count2賦值為0
所以,最終結(jié)果是count1=1 count2=0
我們可以驗(yàn)證一下給這三步換個(gè)順序就很明顯了~


這個(gè)其實(shí)是考驗(yàn)我們對(duì)虛擬機(jī)類加載過(guò)程的了解,先畫個(gè)圖。


類加載過(guò)程

一、加載
虛擬機(jī)規(guī)范要求,虛擬在這個(gè)階段需要完成以下3件事情:

  1. 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流
  2. 將這個(gè)字節(jié)流所代表的的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
  3. 在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口

加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中。也就是ClassLoader起作用的階段。

二、連接
驗(yàn)證、準(zhǔn)備、解析稱為連接階段。
1、驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。

2、準(zhǔn)備
準(zhǔn)備階段是正是為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存將都在方法區(qū)中進(jìn)行分配。這里的進(jìn)行內(nèi)存分配的類變量是被static修飾的變量,而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)分配在堆中。而final static修飾的會(huì)直接賦值為對(duì)應(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、解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程,比如在方法A中使用方法B,A () {B () ;},這里的B () 就是符號(hào)引用,初學(xué)java時(shí)我們都是知道這是java的引用,以為B指向B方法的內(nèi)存地址,但是這是不完整的,這里的B只是一個(gè)符號(hào)引用,它對(duì)于方法的調(diào)用沒有太多的實(shí)際意義,可以這么認(rèn)為,他就是給程序員看的一個(gè)標(biāo)志,讓程序員知道,這個(gè)方法可以這么調(diào)用,但是B方法實(shí)際調(diào)用時(shí)是通過(guò)一個(gè)指針指向B方法的內(nèi)存地址,這個(gè)指針才是真正負(fù)責(zé)方法調(diào)用,他就是直接引用。

三、初始化
初始化階段是類加載過(guò)程的最后一步,前面的類加載過(guò)程中,除了在加載階段用戶應(yīng)用程序可以通過(guò)自定義類加載器參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制,到了初始化階段,才真正開始執(zhí)行勒種定義的Java程序代碼。

初始化結(jié)束后一個(gè)類的加載就結(jié)束了。這個(gè)時(shí)候我們的Class類和類變量(static變量)已經(jīng)在方法區(qū)了。

這個(gè)時(shí)候我們?cè)倩剡^(guò)頭來(lái)看上面的那個(gè)題其實(shí)就很清晰了,其實(shí)就是一個(gè)初始化階段賦值順序的問(wèn)題。

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); 
    }
}

我們來(lái)分析下這里主要涉及的類加載的階段:
一、加載
① 加載SingleTon類
二、連接
1、驗(yàn)證
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()方法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容