Java對(duì)象創(chuàng)建的過程流程分析
一、類加載過程

- 類加載檢查
- 當(dāng)Java虛擬機(jī)(JVM)遇到
new關(guān)鍵字時(shí),它會(huì)先檢查要?jiǎng)?chuàng)建的對(duì)象類是否已經(jīng)被加載、鏈接和初始化。如果尚未加載,JVM會(huì)通過類加載器(ClassLoader)加載對(duì)應(yīng)類的.class文件。
- 當(dāng)Java虛擬機(jī)(JVM)遇到
- 類加載
- 類加載包括三個(gè)子步驟:加載、連接、初始化。
- 加載:通過權(quán)限定類名,讀取 class 文件內(nèi)容為二進(jìn)制流;二進(jìn)制流轉(zhuǎn)換成方法區(qū)(永久代或元數(shù)據(jù)區(qū))的運(yùn)行時(shí) C++類字節(jié)碼對(duì)象 Klass;最后再在堆上生成一個(gè) Class 對(duì)象,用來間接獲取元數(shù)據(jù)區(qū)的類定義信息,靜態(tài)對(duì)象也保存在 Class 對(duì)象中。
- 連接:
- **驗(yàn)證:**驗(yàn)證文件格式、字節(jié)碼元數(shù)據(jù)、語法、符號(hào)引用;
- 準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存,并賦予默認(rèn)初始值(例如0或null),但不會(huì)執(zhí)行任何實(shí)際的初始化代碼。
- **解析:**將符號(hào)引用替換為直接引用。
- 初始化:類的靜態(tài)變量賦予正確的初始值,執(zhí)行靜態(tài)塊;可能涉及父類初始化。
- 類加載包括三個(gè)子步驟:加載、連接、初始化。
- 內(nèi)存分配
- JVM為新創(chuàng)建的對(duì)象分配內(nèi)存空間。對(duì)象內(nèi)存主要包括對(duì)象頭、實(shí)例數(shù)據(jù)以及可能的對(duì)齊填充。
- 對(duì)象頭:存儲(chǔ)對(duì)象自身的元數(shù)據(jù)如哈希碼、鎖狀態(tài)標(biāo)志、GC分代年齡等信息,以及指向其類元數(shù)據(jù)(Class對(duì)象)的指針。
- 實(shí)例數(shù)據(jù):存儲(chǔ)類中定義的字段的實(shí)際數(shù)據(jù)。
- 對(duì)齊填充:非必須,為了滿足JVM對(duì)內(nèi)存地址對(duì)其的要求而填充的額外空間。
- JVM為新創(chuàng)建的對(duì)象分配內(nèi)存空間。對(duì)象內(nèi)存主要包括對(duì)象頭、實(shí)例數(shù)據(jù)以及可能的對(duì)齊填充。
- 初始化零值:
- 分配內(nèi)存后,JVM會(huì)對(duì)對(duì)象的所有字段(包括實(shí)例變量)分配默認(rèn)的初始值。
- 顯式初始化:接下來,如果有在字段聲明時(shí)直接賦予的初始值(例如int a = 10;),這些值會(huì)在構(gòu)造函數(shù)執(zhí)行前被賦予相應(yīng)的變量。
- **對(duì)象頭 必要信息設(shè)置 **主要是對(duì)象頭中類的源數(shù)據(jù)信息,哈希碼 ,對(duì)象 GC 分代年齡等。
- 初始化
- 構(gòu)造器初始化:調(diào)用類的構(gòu)造方法(即構(gòu)造器)進(jìn)行初始化,執(zhí)行構(gòu)造器中的初始化代碼,此時(shí)才會(huì)給實(shí)例變量賦予程序員指定的初始值。
- 如果類中有父類并且還沒有被初始化,則先初始化父類。
- 對(duì)象構(gòu)造完成:
- 構(gòu)造方法執(zhí)行完畢后,對(duì)象就完全構(gòu)造出來了,可以被程序正常使用。
二、對(duì)象內(nèi)存分配方式
內(nèi)存分配方式根據(jù)不同的收集器策略可分為兩種,不同的收集器的堆內(nèi)存規(guī)整程度不一致所以有兩種分配策略。
指針碰撞 Bump The Pointer
在使用指針碰撞策略時(shí),Java堆被假設(shè)為一個(gè)連續(xù)的內(nèi)存空間,被分為已用和未用兩部分,中間由一個(gè)指針作為分界線。當(dāng)新對(duì)象需要內(nèi)存時(shí),JVM只需將指針向未用空間一側(cè)移動(dòng)與對(duì)象大小相等的距離即可。這種方式適用于使用標(biāo)記-清除或復(fù)制算法的垃圾收集器,因?yàn)檫@些算法能夠整理出連續(xù)的內(nèi)存空間。
空閑列表 Free List
如果Java堆中的內(nèi)存不是連續(xù)的,或者已被使用的內(nèi)存和未被使用的內(nèi)存相互交錯(cuò)(這種情況通常發(fā)生在使用標(biāo)記-整理或分代收集算法的垃圾收集器中),那么空閑列表策略更為適用。在這種情況下,JVM維護(hù)一個(gè)列表來記錄堆中各個(gè)小塊的可用內(nèi)存空間。當(dāng)新對(duì)象需要內(nèi)存時(shí),JVM會(huì)從列表中找到一個(gè)足夠大的空閑塊分配給對(duì)象,并更新列表。這種方法不需要連續(xù)的內(nèi)存空間,但管理成本相對(duì)較高。
三、內(nèi)存分配的安全問題
堆是線程間共享的一塊兒區(qū)域,所以多個(gè)線程同時(shí)創(chuàng)建對(duì)象時(shí)都涉及對(duì)內(nèi)存空間的申請(qǐng)和分配,那么內(nèi)存分配就可能出現(xiàn)線程安全問題。依賴以下機(jī)制解決多線程安全問題,一種是** CAS 樂觀鎖機(jī)制**,一種是 TLAB 本地線程分配緩沖機(jī)制
Thread Local Allocation Buffer (TLAB) 本地線程分配緩沖:每個(gè)線程有一個(gè)屬于自己的預(yù)分配內(nèi)存空間,JVM 首先通過 CAS 為線程申請(qǐng)一塊兒預(yù)分配內(nèi)存。這樣當(dāng)某個(gè)線程需要申請(qǐng)新的內(nèi)存空間時(shí)首先現(xiàn)在自己的 TLAB 上分配,能減少內(nèi)存分配沖突。后續(xù) TLAB 內(nèi)存不足了才會(huì) CAS 申請(qǐng)一塊兒新的 LATB 或者直接在 Eden 區(qū)直接分配。
Compare-and-Swap (CAS) 在某些JVM實(shí)現(xiàn)中,可能會(huì)使用CAS操作來實(shí)現(xiàn)無鎖的線程安全內(nèi)存分配。CAS是一種硬件級(jí)別的原子操作,允許線程在不加鎖的情況下比較并交換內(nèi)存中的值,從而減少鎖帶來的性能開銷,并能有效防止數(shù)據(jù)競(jìng)爭。
四、對(duì)象如何進(jìn)入老年代
- 新生代:剛創(chuàng)建的對(duì)象默認(rèn)進(jìn)入新生代的 Eden 區(qū)
- 進(jìn)入老年代的條件:四種情況
- 熬過了多次 minorGC ,每次 MinorGC 過后對(duì)象的年齡就會(huì)+1,存活超過 15 次之后就會(huì)進(jìn)入老年代。該次數(shù)可通過參數(shù)控制
-XX:MaxTenuringThreshold - 動(dòng)態(tài)年齡判斷機(jī)制:MinorGC 后,如果 Survivor 區(qū)中的一批對(duì)象大雨了這塊 Survivor 區(qū)的 50%就會(huì)將大于等于這批對(duì)象年齡最大值的所有對(duì)象直接進(jìn)入老年代。
- 舉例 S1 中有 年齡為 1 、2、3、4 的一批對(duì)象,其中 234 年齡的加起來超過 S1 的 50%,那么年齡大于等于 4 的對(duì)象就直接進(jìn)入老年代了。
- Serial 和 ParNew 收集器,大對(duì)象直接進(jìn)入老年代。例如大字符串和數(shù)組 可通過-XX:PertenureSizeThreshold 配置 默認(rèn)為 1M
- MinorGC 后,存活的對(duì)象太多無法放入 Sruvivor 區(qū)域,會(huì)觸發(fā)空間分配擔(dān)保機(jī)制。將存活的對(duì)象移入老年代
- 熬過了多次 minorGC ,每次 MinorGC 過后對(duì)象的年齡就會(huì)+1,存活超過 15 次之后就會(huì)進(jìn)入老年代。該次數(shù)可通過參數(shù)控制
分配擔(dān)保機(jī)制(空間擔(dān)保) Allocation Assurance Mechanism

什么是分配擔(dān)保機(jī)制
在 JVM 中,空間分配擔(dān)保機(jī)制(Space Allocation Guarantee Mechanism)是一種確保在進(jìn)行垃圾收集時(shí),有足夠的空間來處理對(duì)象晉升和分配的策略。這種機(jī)制主要用于新生代垃圾收集(Minor GC)和老年代垃圾收集(Major GC 或 Full GC)之間的協(xié)調(diào),以避免出現(xiàn)內(nèi)存不足的情況。
:::success
用老年代的空間,來擔(dān)保新生代的垃圾回收可以成功執(zhí)行并騰出空間。會(huì)將新生代存活的對(duì)象轉(zhuǎn)移到老年代中。保證新分配內(nèi)存能直接成功。
- young 分區(qū)內(nèi)存不足以創(chuàng)建新對(duì)象
:::
內(nèi)存擔(dān)保的原理
MinorGC前
- 第一步:判斷老年代可用內(nèi)存是否小于新時(shí)代對(duì)象全部對(duì)象大小,如果小于則繼續(xù)判斷,大于則可進(jìn)行MainorGC
- 第二步:老年代小于存活對(duì)象,則判斷老年代內(nèi)存是否小于每次MinorGC后進(jìn)入老年代的平均大小
- 小于平均大小,則進(jìn)行FullGC,再判斷是否能保存得下存活對(duì)象,放不下則OOM
- 大于平均大小,則進(jìn)行MinorGC
MinorGC后
- 如果存活對(duì)象小于Survivor區(qū),則直接進(jìn)入Survivor區(qū)
- 如果存活對(duì)象大于Survivor區(qū),但是小于老年代可用內(nèi)存,則直接進(jìn)入老年代
- 如果存活對(duì)象大于Survivor區(qū),還大于老年代,則嘗試進(jìn)行一次FullGC,F(xiàn)ullGC后再次判斷,如果放不下存活對(duì)象則會(huì)OOM
分配擔(dān)保的配置
- **-XX:HandlePromotionFailure:**這個(gè)參數(shù)控制是否允許晉升失敗。如果設(shè)置為 true,JVM 會(huì)在 Minor GC 時(shí)嘗試晉升對(duì)象,即使老年代空間不足,也會(huì)嘗試進(jìn)行一次 Minor GC。如果失敗,則觸發(fā) Full GC。這個(gè)參數(shù)在 Java 6 之后已經(jīng)被默認(rèn)取消使用。
- -XX:PretenureSizeThreshold:這個(gè)參數(shù)指定大對(duì)象直接在老年代分配的大小閾值。超過該閾值的對(duì)象直接分配到老年代,避免在新生代頻繁復(fù)制。
- **-XX:MaxTenuringThreshold:**這個(gè)參數(shù)控制對(duì)象在新生代中經(jīng)歷多少次 GC 后晉升到老年代。較高的閾值可以減少對(duì)象晉升,但會(huì)增加新生代的 GC 頻率。
- -XX:TargetSurvivorRatio:這個(gè)參數(shù)控制每次 Minor GC 后目標(biāo)存活區(qū)(Survivor Space)的利用率。JVM 會(huì)根據(jù)這個(gè)參數(shù)調(diào)整對(duì)象晉升的閾值。
五、驗(yàn)證
大對(duì)象直接進(jìn)入老年代
/**
* 測(cè)試:大對(duì)象直接進(jìn)入到老年代
* -Xmx60m -Xms60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* -XX:PretenureSizeThreshold
*
*/
public class YoungOldArea {
public static void main(String[] args) {
byte[] buffer = new byte[1024*1024*20]; //20M
}
}
-XX:NewRatio=2 新生代與老年代比值
-XX:SurvivorRatio=8 新生代中,Eden與兩個(gè)Survivor區(qū)域比值
-XX:+PrintGCDetails 打印詳細(xì)GC日志
-XX:PretenureSizeThreshold 對(duì)象超過多大直接在老年代分配,默認(rèn)值為0,不限制
對(duì)象內(nèi)存分代晉升演示
/*
-Xmx600m -Xms600m -XX:+PrintGCDetails
*/
public class HeapInstance {
public static void main(String[] args) {
List<Picture> list = new ArrayList<>();
while (true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture{
private byte[] pixels;
public Picture(int length){
this.pixels = new byte[length];
}
}

通過可視化插件可以看到
- Eden區(qū)滿了之后,就會(huì)進(jìn)行MinorGC,MinorGC時(shí)會(huì)將Survior放不下的對(duì)象存到old老年代
- 老年代也滿了之后,發(fā)生了三次MinorGC,未釋放出可用空間后,進(jìn)行了三次FullGc最后拋出了OOM
到此這篇關(guān)于Java對(duì)象創(chuàng)建的過程的文章就介紹到這了,更多相關(guān)Java對(duì)象創(chuàng)建內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java對(duì)象創(chuàng)建的幾種方式總結(jié)
- Java對(duì)象創(chuàng)建內(nèi)存案例解析
- 詳解Java對(duì)象創(chuàng)建的過程及內(nèi)存布局
- Hibernate傳入Java對(duì)象創(chuàng)建動(dòng)態(tài)表并錄入數(shù)據(jù)
- java存儲(chǔ)以及java對(duì)象創(chuàng)建的流程(詳解)
- Java創(chuàng)建對(duì)象的四種方式詳解
- JAVA 對(duì)象創(chuàng)建與對(duì)象克隆
- 淺談Java內(nèi)存區(qū)域與對(duì)象創(chuàng)建過程
相關(guān)文章
hotspot解析jdk1.8?Unsafe類park和unpark方法使用
這篇文章主要為大家介紹了hotspot解析jdk1.8?Unsafe類park和unpark方法使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
Java中DTO與Entity拷貝轉(zhuǎn)換的方法小結(jié)
在?Java?開發(fā)中,DTO(Data?Transfer?Object)和?Entity(實(shí)體類)是常見的兩種數(shù)據(jù)模型,本文將介紹幾種常見的工具類和自定義方式來實(shí)現(xiàn)這種轉(zhuǎn)換,感興趣的可以了解下2025-02-02
spring validation多層對(duì)象校驗(yàn)教程
這篇文章主要介紹了spring validation多層對(duì)象校驗(yàn)教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
關(guān)于easyExcel中讀取Excel表頭的實(shí)例說明
EasyExcel是阿里巴巴開源的一個(gè)excel處理框架,以使用簡單、節(jié)省內(nèi)存著稱,下面這篇文章主要給大家介紹了關(guān)于easyExcel中讀取Excel表頭的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06
Java數(shù)據(jù)結(jié)構(gòu)之環(huán)形鏈表和約瑟夫問題詳解
約瑟夫(Josephus)問題是單向環(huán)形鏈表的一種體現(xiàn),也就是丟手帕問題,下面這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)結(jié)構(gòu)之環(huán)形鏈表和約瑟夫問題的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08
Java線程的調(diào)度與優(yōu)先級(jí)詳解
這篇文章主要為大家詳細(xì)介紹了Java線程的調(diào)度與優(yōu)先級(jí),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03
Java面試Socket編程常用參數(shù)設(shè)置源碼問題分析
這篇文章主要為大家介紹了Java編程中關(guān)于Socket結(jié)構(gòu)分析,常用參數(shù)設(shè)置源碼示例以及面試中的問題分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-03-03

