3、JVM中的對象

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 中去拯救

Tags: