在Java編程中,我們知道想要得到一個對象的方式很簡單:new Object() ,但你知道這樣的歲月靜好,卻總有人(JVM)為我們負(fù)重前行。理解這個過程不僅有助于我們編寫更高效的代碼,也能在遇到內(nèi)存問題時快速定位原因。下面我們就來逐步了解Java對象在虛擬機(jī)(JVM)中創(chuàng)建的完整生命周期。一、第一步:類加載檢查 ——"這件商品存在嗎?" 當(dāng) JVM 執(zhí)行到new Object()時,首先要確認(rèn)Object類是否已經(jīng)被加載到內(nèi)存中。這就像我們網(wǎng)購前要先確認(rèn)商品是否在倉庫中存在,JVM 的檢查邏輯可以分為三步: 類名驗(yàn)證:JVM 會從代碼中提取類名 "java.lang.Object",檢查方法區(qū)(JVM 中存儲類信息的內(nèi)存區(qū)域)是否已經(jīng)存在該類的元數(shù)據(jù)(類似商品的說明書)。 加載缺失類:如果Object類未被加載,JVM 會通過類加載器(ClassLoader)從本地 class 文件或 JAR 包中讀取Object.class文件,將其解析為類的元數(shù)據(jù)(包括類的屬性、方法、父類等信息),并存儲到方法區(qū)。驗(yàn)證類信息:為了確保類文件沒有被篡改,JVM 會對加載的Object類進(jìn)行驗(yàn)證,包括字節(jié)碼格式校驗(yàn)、語法檢查等(相當(dāng)于檢查商品包裝是否完好)。對于Object類來說,由于它是 Java 的根類,在 JVM 啟動時就會被提前加載,所以這一步通常會直接跳過加載過程,僅做存在性檢查。但如果是自定義類(如new User()),則會完整執(zhí)行上述類加載流程。 // 第一次使用Object類時觸發(fā)加載 Object obj1 = new Object(); // 第二次使用時類已加載,直接復(fù)用 Object obj2 = new Object();
二、第二步:內(nèi)存分配 ——"給商品找個存放的貨架" 確認(rèn)類已加載后,JVM 需要為新創(chuàng)建的Object對象分配內(nèi)存。內(nèi)存大小在類加載階段就已確定(Object類的實(shí)例占用 16 字節(jié),包括對象頭和實(shí)例數(shù)據(jù)),分配過程就像給商品找合適的貨架位置,有兩種常見方式: - 指針碰撞法:如果 JVM 的堆內(nèi)存是連續(xù)的(就像一排整齊的貨架),已使用內(nèi)存和空閑內(nèi)存之間有一個指針。分配內(nèi)存時,只需將指針向后移動與對象大小相等的距離(相當(dāng)于把貨架上的分隔板后移)。這種方式高效且無內(nèi)存碎片,但要求堆內(nèi)存必須是連續(xù)的,適合 Serial、ParNew 等收集器。
- 空閑列表法:如果堆內(nèi)存中存在大量已使用和空閑的內(nèi)存塊(類似雜亂擺放的貨架),JVM 會維護(hù)一張 "空閑列表",記錄所有可用內(nèi)存塊的位置和大小。分配時從列表中找到足夠大的內(nèi)存塊,劃分出對象所需空間后更新列表(相當(dāng)于從雜亂的貨架中找到合適空間放置新商品)。這種方式適合 CMS 等會產(chǎn)生內(nèi)存碎片的收集器。
在分配內(nèi)存時,JVM 還需要解決多線程并發(fā)問題 —— 避免兩個線程同時分配到同一塊內(nèi)存。就像超市結(jié)賬時需要排隊(duì),JVM 通過兩種方式保證線程安全:要么對內(nèi)存分配過程加鎖(類似排隊(duì)結(jié)賬),要么為每個線程預(yù)先分配一小塊私有內(nèi)存(稱為 TLAB,類似專屬結(jié)賬通道),優(yōu)先在私有內(nèi)存中分配,減少鎖競爭。 三、第三步:內(nèi)存初始化 ——"給貨架做標(biāo)記" 內(nèi)存分配完成后,JVM 會將分配到的內(nèi)存空間(除對象頭外)全部初始化為零值(如 0、null、false)。這一步操作就像給新貨架貼上 "未使用" 的標(biāo)簽,確保對象的實(shí)例字段在沒有顯式賦值時,能有一個默認(rèn)的初始值。 以Object對象為例,由于它沒有自定義屬性,這一步會將 16 字節(jié)內(nèi)存中的實(shí)例數(shù)據(jù)部分全部設(shè)為零。如果是自定義類(如User類有age屬性),則會將age的內(nèi)存區(qū)域初始化為 0,避免出現(xiàn)隨機(jī)垃圾值。這也是為什么 Java 中局部變量必須顯式賦值,而類的成員變量可以直接使用的原因? —— 成員變量的默認(rèn)值在這一步已經(jīng)由 JVM 初始化完成。 四、第四步:設(shè)置對象頭 ——"給商品貼標(biāo)簽" 內(nèi)存初始化后,JVM 需要給對象設(shè)置 "對象頭"(Object Header),這部分?jǐn)?shù)據(jù)相當(dāng)于商品的標(biāo)簽,記錄了對象的核心信息,主要包括: - 運(yùn)行時元數(shù)據(jù):包含對象的哈希碼(相當(dāng)于商品的唯一編號)、GC 分代年齡(記錄對象存活時間,用于垃圾回收)、鎖狀態(tài)標(biāo)志(是否被線程鎖定)等。
- 類型指針:指向?qū)ο笏鶎兕惖脑獢?shù)據(jù)在方法區(qū)的地址(相當(dāng)于商品標(biāo)簽上的 "所屬類別",告訴 JVM 這個對象是Object類的實(shí)例)。
對于Object對象,其對象頭占用 12 字節(jié)(64 位 JVM 默認(rèn)配置),加上 4 字節(jié)的對齊填充(JVM 要求對象大小必須是 8 字節(jié)的整數(shù)倍),總共占用 16 字節(jié)內(nèi)存。 這就是為什么一個空的Object對象會占用 16 字節(jié)內(nèi)存? 答: 對象頭和對齊填充已經(jīng)占滿了空間。 內(nèi)存布局示例: [對象頭] [實(shí)例數(shù)據(jù)] [對齊填充] | 12字節(jié) | 0字節(jié) | 4字節(jié) |
對象頭變化: 對象頭信息會隨著對象狀態(tài)改變而改變: 五、第五步:執(zhí)行構(gòu)造方法 ——"給商品裝內(nèi)容" 完成對象頭設(shè)置后,JVM 會調(diào)用Object類的構(gòu)造方法(<init>()方法)。這一步是真正初始化對象的過程,相當(dāng)于給空貨架填充商品內(nèi)容。 Object類的構(gòu)造方法非常簡單,沒有實(shí)際邏輯(因?yàn)樗鼪]有需要初始化的屬性)。但如果是自定義類,比如User類的構(gòu)造方法需要給name和age賦值,那么 JVM 會在這里執(zhí)行相應(yīng)的賦值操作,將具體的值寫入對象的內(nèi)存空間。 需要注意的是,構(gòu)造方法的執(zhí)行是在類的初始化之后。如果父類還未初始化,JVM 會先觸發(fā)父類的初始化流程(比如User類繼承Person類,會先執(zhí)行Person的構(gòu)造方法),這就是 Java 中 "先有父后有子" 的繼承初始化規(guī)則。 六、第六步:棧幀引用指向?qū)ο?——"記下藥柜位置" 當(dāng)對象在堆內(nèi)存中創(chuàng)建完成后,JVM 會將對象的內(nèi)存地址賦值給棧中的引用變量。這就像我們在醫(yī)院取藥時,藥師會告訴我們 "藥品放在 3 號貨架第 2 層",棧中的引用變量就記錄了對象在堆內(nèi)存中的具體位置。 例如執(zhí)行Object obj = new Object()時,"obj" 是棧中的引用變量,new Object()創(chuàng)建的對象在堆內(nèi)存中,執(zhí)行完這行代碼后,obj 會存儲該對象的內(nèi)存地址,后續(xù)通過obj.xxx調(diào)用方法時,JVM 就能通過這個地址找到堆中的對象。 七、第七步:對象可用 ——"商品上架待售" 經(jīng)過以上六步操作后,Object對象正式創(chuàng)建完成,進(jìn)入可用狀態(tài)。此時 JVM 會繼續(xù)執(zhí)行后續(xù)代碼,對象可以被用來調(diào)用方法(如obj.toString())、作為參數(shù)傳遞等。 當(dāng)這個對象不再被使用(即沒有任何引用指向它),就會被 JVM 的垃圾回收器標(biāo)記為 "可回收",在合適的時機(jī)被清理出內(nèi)存,完成整個生命周期。 看似平凡的一行代碼,實(shí)則凝聚了 JVM 的設(shè)計(jì)智慧 —— 通過將復(fù)雜的底層操作封裝起來,讓開發(fā)者可以專注于業(yè)務(wù)邏輯,這正是 Java 語言 "一次編寫,到處運(yùn)行" 的魅力所在。
|