HotSpot虛擬機(jī)對(duì)象探秘:對(duì)象的創(chuàng)建,我居然看懂了!
引言
大家好,我是你們的老朋友小米,一個(gè)每天都在代碼與咖啡之間切換狀態(tài)的 31 歲程序員。
今天這篇文章,我想帶你一起走進(jìn) Java 世界中一個(gè)特別“魔幻”的角落——對(duì)象創(chuàng)建。
這個(gè)話題你肯定不陌生,對(duì)吧?我們平時(shí)寫代碼:
就這一句,貌似平平無奇,但你有沒有想過,JVM 背后都悄咪咪做了些什么?對(duì)象是怎么一步一步,從“無中生有”被變出來的?HotSpot 是怎么分配內(nèi)存、初始化、搞定 header 的?
今天,小米就帶你走進(jìn) HotSpot 的幕后舞臺(tái),一探對(duì)象創(chuàng)建的秘密。
一切從 new 開始,但 new 只是個(gè)“開關(guān)”
那天我正和同事阿剛喝著下午茶,聊著最近項(xiàng)目優(yōu)化的事,他突然拋來一句:
“你知道 JVM 是怎么創(chuàng)建一個(gè) Java 對(duì)象的嗎?”
當(dāng)時(shí)我一愣,心里想著不就 new 一下嘛,還能有啥。
但等他掏出一張紙開始畫圖的時(shí)候,我才知道,我懂得還遠(yuǎn)遠(yuǎn)不夠。
其實(shí)當(dāng)我們寫下 new User() 這句代碼時(shí),Java 編譯器做的只是把它編譯成了一條字節(jié)碼指令:new。
而真正的魔術(shù),是在 HotSpot 虛擬機(jī)執(zhí)行字節(jié)碼時(shí)才開始的。
這就好比你按下了電梯按鈕,電梯是不是真的來,還得看調(diào)度系統(tǒng)是不是給你安排得當(dāng)。
HotSpot對(duì)象創(chuàng)建的五個(gè)階段,就像造房子
阿剛給我講了一個(gè)特別形象的比喻,他說 JVM 創(chuàng)建對(duì)象就像造房子,大致可以拆解為這五個(gè)步驟:
- 類加載檢查(確定圖紙是否有)
- 內(nèi)存分配(拿地皮)
- 內(nèi)存初始化(打地基)
- 設(shè)置對(duì)象頭(裝修主體)
- 執(zhí)行構(gòu)造方法(軟裝上線)
我當(dāng)時(shí)猛點(diǎn)頭,這不就是咱寫代碼時(shí)那句 new 背后的隱藏劇本嗎!
下面我就一個(gè)個(gè)來講,保證你聽完之后也能在面試官面前吹爆 JVM 對(duì)象創(chuàng)建流程。
第一步:類加載檢查,圖紙有沒有?
每次 new 一個(gè)對(duì)象,JVM 的第一反應(yīng)是:這個(gè)類我加載過沒?
HotSpot 會(huì)通過類加載子系統(tǒng)去檢查類的元數(shù)據(jù)是否已經(jīng)加載、初始化過。
如果沒有?那就先通過雙親委派模型把這個(gè)類加載進(jìn)來。別忘了,類的元信息是在 方法區(qū)(或說元空間 Metaspace) 里的。
沒有圖紙,房子肯定不能造,對(duì)吧。
第二步:內(nèi)存分配,拿地皮
這個(gè)階段,HotSpot 要為新對(duì)象劃一塊地盤,也就是在堆內(nèi)存中“圈一塊地”。
它有兩種分配方式,分別叫做:
- 指針碰撞(bump the pointer)
- 空閑列表(free list)
如果堆內(nèi)存是規(guī)整的,也就是已經(jīng)整理過、不會(huì)有內(nèi)存碎片,JVM 就直接通過一個(gè)叫 alloc_ptr 的指針往前一推,一口氣分配下一塊內(nèi)存,效率超高。
如果堆是不規(guī)整的(比如用了 CMS 垃圾收集器),那 JVM 就得在內(nèi)存中找一個(gè)合適的空閑塊,效率會(huì)稍慢。
但這還不夠,JVM 還得保證線程安全——畢竟多線程下好幾個(gè)線程都可能在同時(shí) new 對(duì)象啊。
于是,它用上了三板斧:
- 通過加鎖(如 TLAB 鎖)
- 通過原子指令 CAS + 重試
- 使用線程本地分配緩沖(TLAB)
大多數(shù)時(shí)候,我們的對(duì)象是在 TLAB(Thread Local Allocation Buffer) 中分配的,線程獨(dú)占,不用加鎖,效率拉滿。
第三步:內(nèi)存初始化,打地基
拿到了地皮之后,HotSpot 下一步就是“清地基”——也就是把剛分配的內(nèi)存區(qū)域清零。
這一步很關(guān)鍵,如果不清零,那對(duì)象的字段可能全是垃圾值,后面出 bug 可就一發(fā)不可收拾了。
清零完畢后,JVM 知道這塊內(nèi)存已經(jīng)是“干凈的”,可以用來創(chuàng)建對(duì)象了。
第四步:設(shè)置對(duì)象頭,裝修主體
HotSpot 中的每個(gè)對(duì)象,都不是裸奔的,它的前面都帶著一個(gè)“對(duì)象頭”。
這個(gè)對(duì)象頭包含了兩類信息:
- Mark Word(標(biāo)記字段):用于存儲(chǔ)對(duì)象的哈希碼、GC 分代年齡、鎖信息等等。
- Klass Pointer:指向?qū)ο笏鶎俚念惖脑獢?shù)據(jù)。
JVM 會(huì)把這些信息填充進(jìn)去,就像把房子主體的水電管線都布好。
這一步結(jié)束,對(duì)象就有了“身份”和“結(jié)構(gòu)”。
第五步:執(zhí)行構(gòu)造方法,軟裝上線
最后一步才是我們 Java 程序員最熟悉的一步:
圖片
JVM 會(huì)根據(jù)類的構(gòu)造函數(shù),逐一執(zhí)行初始化代碼,比如字段賦值、初始化列表等。
這一步,就是給房子裝上窗簾、沙發(fā)、燈具——真正成為一個(gè)“能住人”的家。
“new”背后的思考:為什么你要懂這些?
聽完阿剛那天的分享,我感觸特別深:
我們平時(shí)太容易把對(duì)象創(chuàng)建當(dāng)成“理所當(dāng)然”,卻沒意識(shí)到背后是一套精密的設(shè)計(jì)和優(yōu)化。
尤其在高并發(fā)、內(nèi)存敏感的業(yè)務(wù)場景下,理解對(duì)象創(chuàng)建過程能幫你:
- 識(shí)別性能瓶頸(比如頻繁 GC 的根源可能是對(duì)象創(chuàng)建太多)
- 更高效地使用 TLAB 與逃逸分析
- 編寫更貼合 JVM 優(yōu)化路徑的代碼
比如:用對(duì)象池復(fù)用對(duì)象、避免創(chuàng)建臨時(shí)對(duì)象、使用基本類型替代包裝類型等等。
對(duì)象創(chuàng)建的“高級(jí)玩法”:逃逸分析與棧上分配
其實(shí) HotSpot 還會(huì)進(jìn)一步優(yōu)化對(duì)象的創(chuàng)建,比如當(dāng) JVM 發(fā)現(xiàn)某個(gè)對(duì)象 沒有逃出當(dāng)前方法 時(shí),它甚至?xí)苯釉跅I戏峙鋵?duì)象,而不是堆中。
這叫做:逃逸分析(Escape Analysis)
如果逃逸分析判斷對(duì)象“只在方法內(nèi)部用”,那它就不用堆內(nèi)存了,直接在棧中分配,效率更高,GC 也不會(huì)管它。
你想啊,這對(duì)象一用完方法就銷毀了,壓根不用等 GC。
還有一種優(yōu)化方式叫做:標(biāo)量替換,JVM 會(huì)把整個(gè)對(duì)象拆成若干個(gè)字段來優(yōu)化分配,用得更靈活。
這些優(yōu)化,都得益于你理解對(duì)象是怎么創(chuàng)建的,才能在實(shí)際項(xiàng)目中利用起來。
總結(jié)
說到這,你會(huì)發(fā)現(xiàn),對(duì)象的創(chuàng)建遠(yuǎn)比你想象中復(fù)雜,但每一個(gè)步驟都是為了性能、并發(fā)、安全做出妥協(xié)和平衡。
讓我再用建房子的比喻總結(jié)一次對(duì)象創(chuàng)建的五大步驟:
- 看圖紙(類加載檢查)
- 拿地皮(內(nèi)存分配)
- 打地基(內(nèi)存清零)
- 裝修主體(設(shè)置對(duì)象頭)
- 上軟裝(執(zhí)行構(gòu)造函數(shù))
如果你能理解這些流程,相信在調(diào)優(yōu)性能、理解內(nèi)存問題、寫出更高質(zhì)量代碼時(shí),你會(huì)有意想不到的收獲。
最后,給你一個(gè)小問題
在 JVM 中,如果你創(chuàng)建了 100 萬個(gè)小對(duì)象,它們都短生命周期、只在方法里用,你會(huì)如何優(yōu)化它?