面試官:詳細(xì)完整的說(shuō)說(shuō)對(duì)象實(shí)例化過(guò)程!
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來(lái),我們一起精進(jìn)!你不來(lái),我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!
編輯:業(yè)余草
juejin.cn/post/6919694056071118861
推薦:https://www.xttblog.com/?p=5319

對(duì)象的實(shí)例化過(guò)程需要做哪些工作呢?首先 Java 是一門面向?qū)ο蟮恼Z(yǔ)言,類是對(duì)所屬于一類的所有對(duì)象的抽象,對(duì)象的所有結(jié)構(gòu)化信息都定義在了類中,因此對(duì)象的創(chuàng)建需要根據(jù)類中定義的類型信息,也就是類所對(duì)應(yīng)的 class 二進(jìn)制字節(jié)流,所以這就涉及到了類的加載與初始化。其次,對(duì)象大多存儲(chǔ)在堆內(nèi)存中,這就涉及到內(nèi)存的分配。除此之外,還有變量的初始化零值,對(duì)象頭的設(shè)置,在棧中創(chuàng)建對(duì)象的引用等等,本文我們來(lái)一起詳細(xì)的分析一下對(duì)象的完整實(shí)例化過(guò)程。
整體流程
從整天上來(lái)看對(duì)象的整個(gè)實(shí)例化過(guò)程如下圖所示:

為了故事的順利發(fā)展,這里我們定義一個(gè) Demo,并據(jù)此詳細(xì)討論一下 dc 對(duì)象是如何創(chuàng)建并實(shí)例化出來(lái)的。
public?class?Demo
{
????public?static?void?main(String[]?args)
????{
????????DemoClass?dc=new?DemoClass();
????}
}
class?DemoClass
{
????private?static?final?int?a=1;
????private?static?int?b=2;
????private?static?int?c;
????private?int?d=4;
????private?int?e;
????static
????{
????????c=3;
????}
????public?DemoClass()
????{
????????e=5;
????}
}
類初始化檢查
這里我們使用 new 關(guān)鍵字創(chuàng)建對(duì)象,Java 中創(chuàng)建對(duì)象的方式還有好多種,比如反射,克隆,序列化與反序列化等等。這些方式不一而同,但是經(jīng)過(guò)編譯器編譯之后,對(duì)應(yīng)到 Java 虛擬機(jī)中其實(shí)就是一條 new(這里的 new 指令與前面提到的 new 關(guān)鍵字不同,這是虛擬機(jī)級(jí)別的指令)指令。當(dāng) Java 虛擬機(jī)碰到一條 new 指令時(shí),會(huì)首先根據(jù)這條指令所對(duì)應(yīng)的參數(shù)去常量池中查找是否有該類所對(duì)應(yīng)的符號(hào)引用,并判斷該類是否已經(jīng)被加載、解析、初始化過(guò),也就是到方法區(qū)中檢查是否有該類的類型信息,如果沒(méi)有,首先要進(jìn)行類加載與初始化。如果類已經(jīng)加載和初始化,那么繼續(xù)后續(xù)的操作。
這里假設(shè) DemoClass 類還沒(méi)有被加載與初始化,也就是方法區(qū)中還沒(méi)有 DemoClass 的類型信息,這時(shí)需要進(jìn)行 DemoClass 類的加載與初始化。
類加載過(guò)程
類加載過(guò)程總的可分為7個(gè)步驟:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載。這里我們看一下前六個(gè)階段。
加載
加載階段主要干了三件事:
根據(jù)類的全限定名獲取類的二進(jìn)制字節(jié)流。
將二進(jìn)制字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)中運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
在內(nèi)存中創(chuàng)建一個(gè)代表該類的Java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口。
具體到這里就是首先根據(jù) package.DemoClass 全限定名定位 DemoClass.class 二進(jìn)制文件,然后將該 .class 文件加載到內(nèi)存進(jìn)行解析,將解析之后的結(jié)果存儲(chǔ)在方法區(qū)中,最后在堆內(nèi)存中創(chuàng)建一個(gè) Java.lang.Class 的對(duì)象,用來(lái)訪問(wèn)方法區(qū)中加載的這些類信息。
驗(yàn)證
驗(yàn)證階段完成的任務(wù)主要是確保 class 文件中字節(jié)流中包含的信息符合 Java 虛擬機(jī)的規(guī)范,雖然說(shuō)得很簡(jiǎn)單,但是 Java 虛擬機(jī)進(jìn)行了很多復(fù)雜的驗(yàn)證工作,總的來(lái)說(shuō)可分為四個(gè)方面:
文件格式驗(yàn)證
元數(shù)據(jù)驗(yàn)證
字節(jié)碼驗(yàn)證
符號(hào)引用驗(yàn)證
具體到這里就是對(duì)于加載進(jìn)內(nèi)存的 DemoClass.class 中存儲(chǔ)的信息進(jìn)行虛擬機(jī)級(jí)別的校驗(yàn),以確保 DemoClass.class 中存儲(chǔ)的信息不會(huì)危害到 Java 虛擬機(jī)的運(yùn)行。
準(zhǔn)備
準(zhǔn)備階段完成的工作就是為類變量(也就是靜態(tài)變量)分配內(nèi)存并賦予初始值,通常情況下是變量所對(duì)應(yīng)的數(shù)據(jù)類型的零值。但是在這個(gè)階段,被 final 修飾的變量也就是常量會(huì)在這個(gè)階段準(zhǔn)確的被賦值。
具體到這里,在這個(gè)階段 DemoClass 中的 a 會(huì)被賦值為 1,b 與 c 均被賦值為 0。
解析
這個(gè)階段主要的任務(wù)是將常量池中的符號(hào)引用替換為直接引用。
初始化
在之前的階段中,除了加載階段通過(guò)自定義的類加載器可以干預(yù)虛擬機(jī)的加載過(guò)程外,其他的階段都是虛擬機(jī)完全主導(dǎo),而在初始化階段才開始根據(jù)程序員的意愿執(zhí)行類的初始化,這個(gè)階段主要完成的工作是執(zhí)行類構(gòu)造器方法(),同時(shí)虛擬機(jī)會(huì)保證執(zhí)行該類的類構(gòu)造器方法時(shí),其父類的類構(gòu)造器方法已經(jīng)被正確的執(zhí)行,同時(shí),由于類的初始化只進(jìn)行一次,當(dāng)多個(gè)線程并發(fā)的進(jìn)行初始化時(shí),虛擬機(jī)可以確保多個(gè)線程只有一個(gè)可以完成類的初始化工作, 保證線程安全工作。
具體到 DemoClass類,在這個(gè)階段會(huì)將 b 賦值為 2,c 賦值為 3。
分配內(nèi)存
當(dāng)類加載過(guò)程完成后,或者類本身之前已經(jīng)被加載過(guò),下一步就是虛擬機(jī)要為新生對(duì)象分配內(nèi)存。對(duì)象所需要的內(nèi)存空間在類加載過(guò)程完成后就可以完全確定下來(lái),為對(duì)象分配內(nèi)存空間就相當(dāng)于從堆內(nèi)存中劃分出一塊合適的內(nèi)存來(lái),分配內(nèi)存的主要方式有兩種:指針碰撞和空閑列表。
指針碰撞:這種方式將堆內(nèi)存分為空閑空間與已分配空間,使用一個(gè)指針來(lái)作為二者之間的分界線,當(dāng)要為新生對(duì)象分配內(nèi)存空間的時(shí)候,相當(dāng)于將指針向著空閑空間的方向移動(dòng)一段與對(duì)象大小相等的距離,可見這種分配方式 Java 堆內(nèi)存必須是規(guī)整的,所有空閑空間在一邊,已分配空間在另外一邊。

空閑列表:在虛擬機(jī)中維護(hù)一個(gè)列表,用來(lái)記錄堆中哪一塊內(nèi)存是空閑可用的,在為新生對(duì)象分配內(nèi)存時(shí),從列表中尋找一塊合適大小的可用內(nèi)存塊,分配完成后更新空閑列表,這種方式下堆內(nèi)存的空閑空間與分配空間可以交錯(cuò)存在。

從上面來(lái)看,選擇采用指針碰撞還是空閑列表法分配內(nèi)存,主要由 Java 堆內(nèi)存是否規(guī)整決定的,而 Java 堆內(nèi)存是否規(guī)整又取決于所采用的垃圾收集算法,這就涉及到垃圾回收機(jī)制(可見知識(shí)都是相通的,程序員就是活到老學(xué)到死?。。?,GC 之后是否具有壓縮或者整理的動(dòng)作等等。
同時(shí),由于創(chuàng)建對(duì)象的動(dòng)作是十分頻繁的,多線程可能存在多個(gè)線程同時(shí)申請(qǐng)為對(duì)象分配內(nèi)存空間,這個(gè)時(shí)候如果不采取一定的同步機(jī)制,就有可能導(dǎo)致一個(gè)線程還未來(lái)得及修改指針,另一個(gè)線程就使用了原來(lái)的指針?lè)峙鋬?nèi)存空間,因此衍生出來(lái)了兩種解決方案:CAS 配上失敗重試、TLAB 方式。
第一種方式很好理解,多個(gè)線程使用 CAS 的方式更新指針,多線程下只有一個(gè)線程可以更新完成,其他線程通過(guò)不斷重試完成內(nèi)存指針的重新移動(dòng)。
第二種方式是每個(gè)線程提前分配一塊內(nèi)存空間,這個(gè)內(nèi)存空間就是線程本地緩沖 TLAB,這樣線程每次要分配內(nèi)存時(shí),先去 TLAB 中獲取,當(dāng) TLAB 中內(nèi)存空間不足的時(shí)候才采用同步機(jī)制繼續(xù)申請(qǐng)一塊 TLAB 空間,這樣就降低了同步鎖的申請(qǐng)次數(shù)。
具體到這個(gè)階段,是在堆內(nèi)存中為 DemoClass 對(duì)象,也就是 dc 對(duì)象實(shí)例開辟了一塊內(nèi)存空間。
初始化零值
在為對(duì)象分配內(nèi)存完成之后,虛擬機(jī)會(huì)將分配到的這塊內(nèi)存初始化為零值,這樣也就使得 Java 中的對(duì)象的實(shí)例變量可以在不賦初值的情況下使用,因?yàn)榇a所訪問(wèn)當(dāng)?shù)木褪翘摂M機(jī)為這塊內(nèi)存分配的零值。
具體到這里,就是 Java 虛擬機(jī)將上面分配的內(nèi)存空間初始化為零值,這一步使得現(xiàn)在 DemoClass 中的 d 與 e 均被賦值為 0。
設(shè)置對(duì)象頭
對(duì)象頭就像我們?nèi)说纳矸葑C一樣,存放了一些標(biāo)識(shí)對(duì)象的數(shù)據(jù),也就是對(duì)象的一些元數(shù)據(jù),我們首先看一下對(duì)象的構(gòu)成。

在初始化了零值之后,怎么知道對(duì)象是哪個(gè)類的實(shí)例,就需要設(shè)置指向方法區(qū)中類型信息的指針,對(duì)象 Mark Word 中相關(guān)信息的設(shè)置,就在這個(gè)階段完成。
實(shí)例對(duì)象初始化
這一步虛擬機(jī)將調(diào)用實(shí)例構(gòu)造器方法(),根據(jù)我們程序員的意愿初始化對(duì)象,在這一步會(huì)調(diào)用構(gòu)造函數(shù),完成實(shí)例對(duì)象的初始化。
具體到這里就是 DemoClass 的 d 被賦值為 4,e 被賦值為 5。
創(chuàng)建引用,入棧
執(zhí)行到這一步,堆內(nèi)存中已經(jīng)存在被完成創(chuàng)建完成的對(duì)象,但是我們知道,在 Java 中使用對(duì)象是通過(guò)虛擬機(jī)棧中的引用來(lái)獲取對(duì)象屬性,調(diào)用對(duì)象的方法,因此這一步將創(chuàng)建對(duì)象的引用,并壓如虛擬機(jī)棧中,最終返回引用供我們使用。
在這里就是講對(duì)象的引入入棧,并返回賦值給 dc,至此,一個(gè)對(duì)象被創(chuàng)建完成。
對(duì)象實(shí)例化的完整流程
根據(jù)上面的討論,我們?cè)賮?lái)回顧一下對(duì)象實(shí)例化的整個(gè)流程:

