2、jvm中的對象

一、對象的分配

虛擬機遇到一條new指令時:在常量池中定位到需要new的對象的符號引用,如果未找到,則拋出異常:classNotFoundException

  • 檢查還在
    先執行相應的類加載過程,如果沒有就進行加載

  • 內存分配
    根據方法區的信息確定爲該類分配的內存空間大小
    在這裏插入圖片描述
    指針碰撞 (java 堆內存空間規整的情況下使用)
    如果 Java 堆中內存是絕對規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離,這種分配方式稱爲“指針碰撞”。
    空閒列表 (java 堆空間不規整的情況下使用)
    如果 Java 堆中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護 一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種 分配方式稱爲“空閒列表”。
    選擇哪種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

併發安全
除如何劃分可用空間之外,還有另外一個需要考慮的問題是對象創建在虛擬機中是非常頻繁的行爲,即使是僅僅修改一個指針所指向的位置,在併發情況下也並不是線程安全的,可能出現正在給對象 A 分配內存,指針還沒來得及修改,對象 B 又同時使用了原來的指 針來分配內存的情況。

CAS 機制
解決這個問題有兩種方案,一種是對分配內存空間的動作進行同步處理——實際上虛擬機採用 CAS 配上失敗重試的方式保證更新操作 的原子性;

分配緩衝
另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在 Java 堆中預先分配一小塊私有內存,也就是本地線程 分配緩衝(Thread Local Allocation Buffer,TLAB),如果設置了虛擬機參數 -XX:+UseTLAB,在線程初始化時,同時也會申請一塊指定大小 的內存,只給當前線程使用,這樣每個線程都單獨擁有一個 Buffer,如果需要分配內存,就在自己的 Buffer 上分配,這樣就不存在競 爭的情況,可以大大提升分配效率,當 Buffer 容量不夠的時候,再重新從 Eden 區域申請一塊繼續使用。

TLAB 的目的是在爲新對象分配內存空間時,讓每個 Java 應用線程能在使用自己專屬的分配指針來分配空間(Eden 區,默認 Eden 的 1%), 減少同步開銷。

TLAB 只是讓每個線程有私有的分配指針,但底下存對象的內存空間還是給所有線程訪問的,只是其它線程無法在這個區域分配而已。 當一個 TLAB 用滿(分配指針 top 撞上分配極限 end 了),就新申請一個 TLAB。

  • 內存空間初始化
    (注意不是構造方法)內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(如 int 值爲 0,boolean 值爲 false 等等)。這
    一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

  • 設置
    接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。這些信息存放在對象的對象頭之中。

  • 對象初始化
    在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建纔剛剛開始,所有的字段都還爲零值。所以,一般來說,執行 new 指令之後會接着把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完 全產生出來。

二、對象的內存佈局

在這裏插入圖片描述
在 HotSpot 虛擬機中,對象在內存中存儲的佈局可以分爲 3 塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。 對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等。

對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於 HotSpot VM 的自動內存管理系統要求對對 象的大小必須是 8 字節的整數倍。對象正好是 9 字節的整數,所以當對象其他數據部分(對象實例數據)沒有對齊時,就需要通過對 齊填充來補全。

三、對象的訪問方式

建立對象是爲了使用對象,我們的 Java 程序需要通過棧上的reference數據來操作堆上的具體對象。目前主流的訪問方式有使用句柄和 直接指針兩種。
在這裏插入圖片描述

  • 句柄
    如果使用句柄訪問的話,那麼 Java 堆中將會劃分出一塊內存來作爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。
  • 直接指針
  • 如果使用句柄訪問的話,那麼 Java 堆中將會劃分出一塊內存來作爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了 對象實例數據與類型數據各自的具體地址信息。

四、堆內存分配策略

1、堆的劃分

新生代(PSYoungGen)

  • Eden空間
  • From Survivor空間
  • To Survivor空間

老年代(ParOldGen)

堆中參數配置:
新生代大小: -Xmn20m 表示新生代大小20m(初始和最大)

-XX:SurvivorRatio=8 表示Eden和Survivor的比值,
缺省爲8 表示 Eden:From:To= 8:1:1
2 Eden:From:To= 2:1:1

2、分配的規則

在這裏插入圖片描述

  • 對象優先分配在Eden區
    大多數情況下,對象在新生代 Eden 區中分配。當 Eden 區沒有足夠空間分配時,虛擬機將發起一次 Minor GC。
  • 大對象直接進入老年代
    最典型的大對象是那種很長的字符串以及數組。這樣做的目的:1.避免大量內存複製,2.避免提前進行垃圾回收,明明內存有空間進行分配。
  • 長期存活的對象進入老年代
    如果對象在 Eden 出生並經過第一次 Minor GC 後仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將對象年齡設爲 1,對象在 Survivor區中每熬過一次 Minor GC,年齡就增加 1,當它的年齡增加到一定程度(默認爲 15)_時,就會被晉升到老年代中。
  • 動態年齡判定
    如果在 Survivor 空間中相同年齡所有對象大小的綜合大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代
  • 空間分配擔保
    在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼 Minor GC 可以確保是安全 的。如果不成立,則虛擬機會查看 HandlePromotionFailure 設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷 次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次 Minor GC,儘管這次 Minor GC 是有風險的,如果擔保失敗則會進行一次 Full GC;如果小 於,或者 HandlePromotionFailure 設置不允許冒險,那這時也要改爲進行一次 Full GC。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章