3、JVM中的物件

CarBlack發表於2020-09-29

1、物件的建立

 A  a = new A()

A:引用的型別

a::引用的名稱

new A():建立一個A類物件

當建立一個物件時,具體建立過程是什麼呢?

(1)JVM遇到new的位元組碼指令後,檢查類是否被載入,否,進行類載入

(2)檢查載入通過後,對新建立的物件在堆中分配記憶體

(3)將分配的記憶體空間進行初始化為0值

(4)設定物件頭的資訊,將物件的所屬類(即類的後設資料資訊)、物件的HashCode、物件的GC資訊、鎖資訊等資料儲存在物件頭中

(5)呼叫物件的構造方法進行初始化

2、物件記憶體的分配策略

物件建立的過程中需要為新物件在堆上劃分出一塊確定大小的記憶體空間,JVM中對於劃分記憶體有兩種策略,指標碰撞和空閒列表

指標碰撞:當記憶體空間絕對規整,使用中的記憶體放一邊,未使用的記憶體放另一邊,中間放由一個作為分界器的指標,當進行記憶體分配時,指標向空閒記憶體方向挪動與物件大小相等的距離

空閒列表:當堆上的記憶體空間不是絕對規整,使用和未使用的記憶體空間呈犬牙交錯的形勢,此時虛擬機器需要維護一個列表,列表中記錄了那塊記憶體未被使用,分配記憶體時需要在列表中找到一塊足夠大的記憶體空間或分給新建的物件,並更新表中的記錄。

其中指標碰撞的分配策略效能要更高一些,JVM採用哪種分配策略是由堆上記憶體空間是否絕對規整來決定的,記憶體空間是否絕對規整是由JVM採用哪種GC來決定的

給物件劃分記憶體空間時,不僅要考慮記憶體的分配策略,還需要考慮到記憶體分配時的併發安全,JVM中是怎樣確保記憶體分配時的併發安全呢?

JVM中建立物件十分的頻繁,當物件A建立時,剛為其分配記憶體,還未更新指標或者列表時,物件B來建立,此時就會發生問題

JVM中為了保證併發情況下執行緒安全,採用了兩種方案:CAS失敗重試和分配緩衝

CAS失敗重試:CAS(Compare-and-Swap),即比較並替換,是一種實現併發演算法時常用到的技術,Java併發包(Java.Util.Concurrent)中的原子類都使用了CAS技術。

CAS需要有3個運算元:記憶體地址V,舊的預期值A,即將要更新的目標值B。
CAS指令執行時,當且僅當記憶體地址V的值與預期值A相等時,將記憶體地址V的值修改為B,否則就什麼都不做。整個比較並替換的操作是一個原子操作。

CAS失敗重試流程:一塊空白的記憶體,此時是null值,在空白的記憶體中劃分出一塊與申請物件大小一致的記憶體,劃分完之後,再來看記憶體是否為null,是,為物件分配記憶體成功,否,說明在劃分的過程中有別的執行緒來對這塊記憶體進行了分配的操作,為物件分配記憶體失敗,找到下一塊空白的記憶體,繼續上述操作

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

TLAB 的目的是在為新物件分配記憶體空間時,讓每個 Java 應用執行緒能在使用自己專屬的分配指標來分配空間,減少同步開銷。
TLAB 只是讓每個執行緒有私有的分配指標,但底下存物件的記憶體空間還是給所有執行緒訪問的,只是其它執行緒無法在這個區域分配而已。當一個 TLAB 用滿(分配指標 top 撞上分配極限 end 了),就新申請一個 TLAB。
設定引數:-XX:+UseTLAB 允許在年輕代空間中使用執行緒本地分配塊(TLAB)預設使用
     -XX:-UseTLAB 禁用記憶體分配
3、物件的分配策略
堆上分配
大多數情況下,物件在新生代 Eden 區中分配。當 Eden 區沒有足夠空間分配時,虛擬機器將發起一次 Minor GC
但是當物件太大時,需要大量連續記憶體空間,此時會被分配到老年代,這樣可以避免GC時新生代採用複製演算法時走出的大量記憶體複製操作,,避免明明記憶體有空間進行分配而提前進行垃圾回收
棧上分配
在方法中建立的基本資料型別的物件會被分配到棧上,最好開啟逃逸分析
逃逸分析的原理:分析物件動態作用域,當一個物件在方法中定義後,它可能被外部方法所引用
比如:呼叫引數傳遞到其他方法中,這種稱之為方法逃逸。甚至還有可能被外部執行緒訪問到,例如:賦值給其他執行緒中訪問的變數,這個稱之為執行緒逃逸
從不逃逸到方法逃逸到執行緒逃逸,稱之為物件由低到高的不同逃逸程度
如果確定一個物件不會逃逸出執行緒之外,那麼讓物件在棧上分配記憶體可以提高 JVM 的效率
如果是逃逸分析出來的物件可以在棧上分配的話,那麼該物件的生命週期就跟隨執行緒了,就不需要垃圾回收,如果是頻繁的呼叫此方法則可以得到很大的效能提高
4、物件結構

 

物件大小為8的整數倍,方便記憶體的劃分

5、如何定位物件

定位物件的方式有兩種:控制程式碼和直接指標

控制程式碼:JVM在堆上劃分出一塊記憶體作為控制程式碼池,引用(reference)中儲存的物件就是控制程式碼的地址,控制程式碼中包含了物件的例項資料與型別資料真實的地址資訊

   優點:引用 中儲存的是穩定的控制程式碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制程式碼中的例項資料指標,而引用本身不需要修改

直接指標:引用(reference)中儲存的物件就是真實地址,Sun HotSpot 是使用直接指標訪問方式進行物件訪問的

     優點:較比控制程式碼速度要快一些,因為它節省了一次指標定位的時間開銷

6、引用的型別

強引用:一般的 Object obj = new Object() ,就屬於強引用。在任何情況下,只有有強引用關聯(與根可達)還在,垃圾回收器就永遠不會回收掉被引用的物件

軟引用:一些有用但是並非必需,用軟引用關聯的物件,系統將要發生記憶體溢位(OuyOfMemory)之前,這些物件就會被回收(如果這次回收後還是沒有足夠的

空間,才會丟擲記憶體溢位)

弱引用:一些有用(程度比軟引用更低)但是並非必需,用弱引用關聯的物件,只能生存到下一次垃圾回收之前,GC 發生時,不管記憶體夠不夠,都會被回收

虛引用:幽靈引用,最弱(隨時會被回收掉)

7、如何判斷物件是否存活

判斷物件是否存活的方式有兩種:引用計數法和可達性分析

引用計數法:在物件中新增一個引用計數器,每當有一個地方引用它,計數器就加 1,當引用失效時,計數器減 1

      這種方法Python中使用,JVM中沒有使用

可達性分析:通過以GC Roots物件為起點,向下搜尋,看是否存在引用,搜尋走過的路被稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈,說明此物件是無用可被回收的

      GC Roots物件:虛擬機器棧(棧幀中的本地變數表)中引用的物件

             各個執行緒呼叫方法堆疊中使用到的引數、區域性變數、臨時變數等

             方法區中類靜態屬性引用的物件
             java 類的引用型別靜態變數
             方法區中常量引用的物件,比如:字串常量池裡的引用
             本地方法棧中 JNI(即一般說的 Native 方法)引用的物件
             JVM 的內部引用(class 物件、異常物件 NullPointException、OutofMemoryError,系統類載入器)
             所有被同步鎖(synchronized 關鍵)持有的物件
             JVM 內部的 JMXBean、JVMTI 中註冊的回撥、原生程式碼快取等
             JVM 實現中的“臨時性”物件,跨代引用的物件

Finalize方法:即使通過可達性分析判斷不可達的物件,也不是“非死不可”,它還會處於“緩刑”階段,真正要宣告一個物件死亡,需要經過兩次標記過程,一次是沒有找到與 GCRoots 的引用鏈,它將被第一次標記。隨後進行一次篩選(如果物件覆蓋了 finalize),我們可以在 finalize 中去拯救

相關文章