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

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

什么是分配擔保機制
在 JVM 中,空間分配擔保機制(Space Allocation Guarantee Mechanism)是一種確保在進行垃圾收集時,有足夠的空間來處理對象晉升和分配的策略。這種機制主要用于新生代垃圾收集(Minor GC)和老年代垃圾收集(Major GC 或 Full GC)之間的協(xié)調(diào),以避免出現(xiàn)內(nèi)存不足的情況。
:::success
用老年代的空間,來擔保新生代的垃圾回收可以成功執(zhí)行并騰出空間。會將新生代存活的對象轉(zhuǎn)移到老年代中。保證新分配內(nèi)存能直接成功。
- young 分區(qū)內(nèi)存不足以創(chuàng)建新對象
:::
內(nèi)存擔保的原理
MinorGC前
- 第一步:判斷老年代可用內(nèi)存是否小于新時代對象全部對象大小,如果小于則繼續(xù)判斷,大于則可進行MainorGC
- 第二步:老年代小于存活對象,則判斷老年代內(nèi)存是否小于每次MinorGC后進入老年代的平均大小
- 小于平均大小,則進行FullGC,再判斷是否能保存得下存活對象,放不下則OOM
- 大于平均大小,則進行MinorGC
MinorGC后
- 如果存活對象小于Survivor區(qū),則直接進入Survivor區(qū)
- 如果存活對象大于Survivor區(qū),但是小于老年代可用內(nèi)存,則直接進入老年代
- 如果存活對象大于Survivor區(qū),還大于老年代,則嘗試進行一次FullGC,F(xiàn)ullGC后再次判斷,如果放不下存活對象則會OOM
分配擔保的配置
- **-XX:HandlePromotionFailure:**這個參數(shù)控制是否允許晉升失敗。如果設(shè)置為 true,JVM 會在 Minor GC 時嘗試晉升對象,即使老年代空間不足,也會嘗試進行一次 Minor GC。如果失敗,則觸發(fā) Full GC。這個參數(shù)在 Java 6 之后已經(jīng)被默認取消使用。
- -XX:PretenureSizeThreshold:這個參數(shù)指定大對象直接在老年代分配的大小閾值。超過該閾值的對象直接分配到老年代,避免在新生代頻繁復制。
- **-XX:MaxTenuringThreshold:**這個參數(shù)控制對象在新生代中經(jīng)歷多少次 GC 后晉升到老年代。較高的閾值可以減少對象晉升,但會增加新生代的 GC 頻率。
- -XX:TargetSurvivorRatio:這個參數(shù)控制每次 Minor GC 后目標存活區(qū)(Survivor Space)的利用率。JVM 會根據(jù)這個參數(shù)調(diào)整對象晉升的閾值。
五、驗證
大對象直接進入老年代
/**
* 測試:大對象直接進入到老年代
* -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與兩個Survivor區(qū)域比值
-XX:+PrintGCDetails 打印詳細GC日志
-XX:PretenureSizeThreshold 對象超過多大直接在老年代分配,默認值為0,不限制
對象內(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ū)滿了之后,就會進行MinorGC,MinorGC時會將Survior放不下的對象存到old老年代
- 老年代也滿了之后,發(fā)生了三次MinorGC,未釋放出可用空間后,進行了三次FullGc最后拋出了OOM
到此這篇關(guān)于Java對象創(chuàng)建的過程的文章就介紹到這了,更多相關(guān)Java對象創(chuàng)建內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實現(xiàn)阿里云短信發(fā)送的示例代碼
這篇文章主要為大家介紹了如何利用SpringBoot實現(xiàn)阿里云短信發(fā)送,文中的示例代碼講解詳細,對我們學習或工作有一定幫助,需要的可以參考一下2022-04-04
使用maven一步一步構(gòu)建spring mvc項目(圖文詳解)
這篇文章主要介紹了詳解使用maven一步一步構(gòu)建spring mvc項目,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09
Eclipse 2020-06 漢化包安裝步驟詳解(附漢化包+安裝教程)
這篇文章主要介紹了Eclipse 2020-06 漢化包安裝步驟(附漢化包+安裝教程),本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
在IDEA中搭建最小可用SpringMVC項目(純Java配置)
這篇文章主要介紹了在IDEA中搭建最小可用SpringMVC項目(純Java配置),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-12-12

