Java對象創建過程


基本概念

指針碰撞

一般情況下,JVM的對象都放在堆內存中(發生逃逸分析除外)。當類加載檢查通過后,Java虛擬機開始為新生對象分配內存。如果Java堆中內存是絕對規整的,所有被使用過的的內存都被放到一邊,空閑的內存放到另外一邊,中間放着一個指針作為分界點的指示器,所分配內存僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的實例,這種分配方式就是指針碰撞.

空閑列表

如果Java堆內存中的內存並不是規整的,已被使用的內存和空閑的內存相互交錯在一起,不可以進行指針碰撞啦,虛擬機必須維護一個列表,記錄哪些內存是可用的,在分配的時候從列表找到一塊大的空間分配給對象實例,並更新列表上的記錄,這種分配方式就是空閑列表

用何種方式要基於虛擬機堆內存是否規整,這又由采用的垃圾收集器是否帶有壓縮整理功能決定,所以類似Serial、ParNes等收集器時采用指針碰撞,而采用CMS這種基於Mark-Sweep算法的收集器時采用空閑列表。

TLAB(Thread Local Allocation Buffer,本地線程分配緩存)

TLAB 可以把內存分配的動作按照線程划分在不同的空間中,每個線程在Java堆中預先分配一小塊內存,這就是TLAB

虛擬機通過-XX:UseTLAB設定它的,java層面與之對應的是ThreadLocal類的實現

分配過程

  • 將所有內存分配的操作划分到多個不同線程中進行,預先從Eden區給每個線程分配一小塊內存(默認Eden區的1%),稱為TLAB
  • 哪個線程需要分配內存,先在那一個線程的TLAB上進行分配
  • 當線程的TLAB用完了或者TLAB剩余內存不足以存放對象時,向Eden區重新申請TLAB,再次嘗試分配
  • 如果放不下,則采用CAS的方式進行同步鎖定,在Eden區嘗試分配
  • 如果還是放不下,在Eden區執行Young GC(minor GC),在Eden區嘗試分配
  • 如果還是放不下,則將對象分配至老年代

內存分配並發問題

對象創建在虛擬機中屬於頻繁操作,這就涉及到了並發操作(當給對象A分配內存並且還沒有分配完畢時,給對象B分配相同的的內存區域)。

解決方案主要包括兩種:CAS和TLAB

  • CAS (compare and swap)是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性。在這個內存分配上,CAS失敗再來一次的時候要再去讀取一遍old值。(實際上發現該位置有值了就不會再在該位置分配了,會去下一個空閑空間分配。)
  • TLAB 可以把內存分配的動作按照線程划分在不同的空間之中進行,每個線程在Java堆中預先分配一小塊內存。當對象大於TLAB中的剩余內存或TLAB的內存已用盡時,再采用上述的CAS進行內存分配。

創建對象過程(new方法創建對象)

類加載檢測

當虛擬機收到一條new指令時,首先將檢查當前new的類是否在常量池被加載過(在常量池找到需要new的類的符號,檢查其是否被初始化過)。如果沒有,則執行相應的類加載過程;如果有則直接准備為新的對象分配內存。

為新生對象分配內存

對象所需內存大小在類加載完成后就已確定,分配內存的過程等同於將一塊確定大小的內存從java堆划分出來。分配方式有指針碰撞空閑列表兩種,選擇哪種分配方式由Java堆是否規整決定;Java堆是否規整又由選擇的GC收集器是否帶有壓縮整理功能決定

初始化缺省值

  • 在內存分配完畢后,需要對分配后的內存空間初始化為缺省值(如果使用TLAB,則提前至分配TLAB時進行)。**這一步保證了Java對象無需賦值即可直接使用,程序能訪問到這些字段的數據類型的缺省值。

  • 抽象數據類型默認初始化為null,基本數據類型為0,布爾為false...

設置對象必要參數

  • 對對象的對象頭(Object Header)進行初始化,包括類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息存放在對象頭中,某些參數,比如是否啟用偏向鎖等,對象頭會有不同的設置方式。

執行對象構造(init)方法

  • 對於虛擬機來說,在執行init方法前,對象的創建已經結束了。但對於java程序來說,創建對象的過程還沒有結束(在執行init方法之前,所有字段值均為零)。執行new指令后,會接着執行init方法,把對象按照程序員的想法進行初始化,才算完成了一個對象的創建。

Java對象分配

  • 1)依據逃逸分析,判斷是否能棧上分配?

    • 如果可以,使用標量替換方式,把對象分配到VM Stack中。如果 線程銷毀或方法調用結束后,自動銷毀,不需要 GC 回收器 介入。
    • 否則,繼續下一步。
  • 2)判斷是否大對象?

    • 如果是,直接分配到堆上 Old Generation 老年代上。如果對象變為垃圾后,由老年代GC 收集器(比如 Parallel Old, CMS, G1)回收。
    • 否則,繼續下一步。
  • 3)判斷是否可以在TLAB中分配?

    • 如果是,在 TLAB中分配堆上Eden區。
    • 否則,在 TLAB外堆上的Eden區分配。
    1. 動態年齡判斷
    • 大於等於某個年齡的對象超過了survivor空間一半 ,大於等於某個年齡的對象直接進入老年代
    • 否則,繼續下一步
  • 5)分配到Eden區

整理歸納對象創建整體過程

整體:

  1. 首次創建對象時,類中的靜態方法/靜態字段首次被訪問時,Java 解釋器必須先查找類路徑,以定位 .class 文件;
  2. 然后載入 .class(這將創建一個 Class 對象),有關靜態初始化的所有動作都會執行。因此,靜態初始化只在 Class 對象首次加載的時候進行一次;
  3. 當用 new 方法創建對象時,首先再堆上為對象分配足夠的存儲空間;
  4. 這塊存儲空間會被清零,這就自動地將對象中的所有基本類型數據都設置成了缺省值(對數字來說就是 0,對 boolean 和 str 也相同),而引用則被設置成了 null;
  5. 執行所有出現於字段定義處的初始化動作(非靜態對象的初始化);
  6. 執行構造器。

init方法:

Java 在編譯之后會在字節碼文件中生成 init 方法,稱之為實例構造器,該實例構造器會將語句塊,變量初始化,調用父類的構造器等操作收斂到 init 方法中,收斂順序為:

  1. 父類變量初始化
  2. 父類語句塊
  3. 父類構造函數
  4. 子類變量初始化
  5. 子類語句塊
  6. 子類構造函數

clinit 方法

Java 在編譯之后會在字節碼文件中生成 clinit 方法,稱之為類構造器。類構造器同實例構造器一樣,也會將靜態語句塊,靜態變量初始化,收斂到 clinit 方法中,收斂順序為:

  1. 父類靜態變量初始化
  2. 父類靜態語句塊
  3. 子類靜態變量初始化
  4. 子類靜態語句塊

若父類為接口,則不會調用父類的 clinit 方法。一個類可以沒有 clinit 方法。

clinit 方法是在類加載過程中執行的,而 init 是在對象實例化執行的,所以 clinit 一定比 init 先執行。

特殊情形:

對象不一定是使用new進行實例化的,也可以是其他方式.

  • 如果對象是通過 clone() 方法創建的,那么 JVM 把原來被克隆的對象的實例變量的值拷貝到新對象中
  • 如果對象是通過 ObjectInputStream 類的 readObject() 方法創建的,那么 JVM 通過從輸入流中讀入的序列化數據來初始化那些非暫時性(non-transient)的實例變量;

參考:

JVM 中對象咋創建啊,又怎么訪問啊

jvm 優化篇-(5)-線程局部緩存TLAB 指針碰撞、Eden區分配 -XX:+UseTLAB -XX:+PrintTLAB -XX:TLABWasteTargetPercent

JVM對象的創建過程—— 以HotSpot為例

JVM 中對象咋創建啊,又怎么訪問啊

Java對象的創建過程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM