JVM 中的對象及引用


JVM中對象的創建過程

 

 

 

對象的內存分配

   虛擬機遇到一條 new 指令時,首先檢查是否被類加載器加載,如果沒有,那必須先執行相應的類加載過程。

  類加載就是把 class 加載到 JVM 的運行時數據區的過程。

 

1)檢查加載

 

首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用(符號引用 符號引用以一組符號來描述所引用的目標),並且檢查類是否已經被加載、解析和初始化過。

 

2)分配內存

 

接下來虛擬機將為新生對象分配內存。為對象分配空間的任務等同於把一塊確定大小的內存從 Java 堆中划分出來

指針碰撞

如果 Java 堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放着一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”。

空閑列表

如果 Java 堆中的內存並不是規整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄,這種分配方式稱為“空閑列表”。

 

 

 

選擇哪種分配方式由 Java 堆是否規整決定,而 Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。

如果是 Serial、ParNew 等帶有壓縮的整理的垃圾回收器的話,系統采用的是指針碰撞,既簡單又高效

如果是使用 CMS 這種不帶壓縮(整理)的垃圾回收器的話,理論上只能采用較復雜的空閑列表。

並發安全

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

CAS機制

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

分配緩沖

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

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

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

參數

-XX:+UseTLAB

允許在年輕代空間中使用線程本地分配塊(TLAB)。默認情況下啟用此選項。要禁用 TLAB,請指定-XX:-UseTLAB。

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

 

3)內存空間初始化

(注意不是構造方法)內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(如 int 值為 0,boolean 值為 false 等等)。這一步操作保證了對象

的實例字段在 Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

4)設置

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

5)對象初始化

在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建才剛剛開始,所有的字段都還為零值。

所以,一般來說,執行 new 指令之后會接着把對象按照程序員的意願進行初始化(構造方法),這樣一個真正可用的對象才算完全產生出來。

對象的內存布局

 

 

 

在 HotSpot 虛擬機中,對象在內存中存儲的布局可以分為 3 塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC 分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。

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

如果對象是一個 java數組,那么在對象頭中還有一塊用於記錄數組長度的數據。

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

 

 

 

對象的訪問定位

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

句柄

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

使用句柄來訪問的最大好處就是 reference 中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而 reference 本身不需要修改.

直接指針

如果使用直接指針訪問,reference 中存儲的直接就是對象地址

這兩種對象訪問方式各有優勢,使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在 Java中非常頻繁,因此這類開銷積少成多后也是一項非常可觀的執行成本。

對 Sun HotSpot 而言,它是使用直接指針訪問方式進行對象訪問的。

判斷對象的存活

在堆里面存放着幾乎所有的對象實例,垃圾回收器在對對進行回收前,要做的事情就是確定這些對象中哪些還是“存活”着,哪些已經“死去”(死去代表着不可能再被任何途徑使用得對象了)

什么是垃圾?

語言申請內存:malloc free

C++:new delete

C/C++ 手動回收內存

Java: new

Java是自動內存回收,編程上簡單,系統不容易出錯。

手動釋放內存,容易出兩種類型的問題:

1、忘記回收

2、多次回收

沒有任何引用指向的一個對象或者多個對象(循環引用)

引用計數法

在對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1,當引用失效時,計數器減 1.

 

 

 

Python 在用,但主流虛擬機沒有使用,因為存在對象相互引用的情況,這個時候需要引入額外的機制來處理,這樣做影響效率,

 

 

 

 

在代碼中看到,只保留相互引用的對象還是被回收掉了,說明 JVM 中采用的不是引用計數法

可達性分析

來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。

作為 GC Roots 的對象包括下面幾種(重點是前面 4 種)

虛擬機棧(棧幀中的本地變量表)中引用的對象;各個現場被調用方法堆棧中使用到的參數、局部變量、臨時變量等。

方法區中類靜態屬性引用的對象;java 類的引用類型靜態變量

方法區中常量引用的對象;比如:字符串常量池里的引用。

本地方法棧中 JNI(即一般說的 Native 方法)引用的對象

l JVM 的內部引用(class 對象、異常對象 NullPointException、OutofMemoryError,系統類加載器)。(非重點)

所有被同步鎖(synchronized 關鍵)持有的對象。(非重點)

l JVM 內部的 JMXBean、JVMTI 中注冊的回調、本地代碼緩存等(非重點)

l JVM 實現中的“臨時性”對象,跨代引用的對象

 

以上的回收都是對象,類的回收條件:

注意 Class 要被回收條件比較苛刻必須同時滿足以下的條件(僅僅是可以不代表必然因為還有一些參數可以進行控制):

1、該類所有的實例都已經被回收,也就是堆中不存在該類的任何實例。

2、加載該類的 ClassLoader 已經被回收

3、該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

4、參數控制:

 

 

 

Finalize方法

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

代碼演示

 

 

 

 

運行結果

 

 

 

可以看到對象可以被拯救一次(finalize執行第一次但是不會執行第二次)代碼改一下再來一次

 

 

 

運行結果

對象沒有被拯救這個就是 finalize方法執行緩慢還沒有完成拯救垃圾回收器就已經回收掉了

所以建議大家盡量不要使用 finalize因為這個方法太不可靠在生產中你很難控制方法的執行或者對象的調用順序建議大家忘了 finalize 方法!因為在finalize方法能做的工作java中有更好的比如 try-finally 或者其他方式可以做得更好

 

 

各種引用

強引用

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

軟引用 SoftReference

一些有用但是並非必需,用軟引用關聯的對象,系統將要發生內存溢出(OuyOfMemory)之前,這些對象就會被回收(如果這次回收后還是沒有足夠的

空間,才會拋出內存溢出)。參見代碼:

VM 參數 -Xms10m -Xmx10m -XX:+PrintGC

 

運行結果

例如,一個程序用來處理用戶提供的圖片。如果將所有圖片讀入內存,這樣雖然可以很快的打開圖片,但內存空間使用巨大,一些使用較少的圖片浪費內存空間,需要手動從內存中移除。如果每次打開圖片都從磁盤文件中讀取到內存再顯示出來,雖然內存占用較少,但一些經常使用的圖片每次打開都要訪問磁盤,代價巨大。這個時候就可以用軟引用構建緩存。

 

弱引用 WeakReference

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

參看代碼:

 

注意軟引用 SoftReference 和弱引用 WeakReference,可以用在內存資源緊張的情況下以及創建不是很重要的數據緩存。當系統內存不足的時候,緩存中的內容是可以被釋放的。

實際運用(WeakHashMap、ThreadLocal)

 

虛引用 PhantomReference

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

垃圾回收的時候收到一個通知,就是為了監控垃圾回收器是否正常工作。

 

對象的分配策略

棧上分配

 

沒有逃逸

即方法中的對象沒有發生逃逸。

逃逸分析的原理分析對象動態作用域,當一個對象在方法中定義后,它可能被外部方法所引用。

比如:調用參數傳遞到其他方法中,這種稱之為方法逃逸。甚至還有可能被外部線程訪問到,例如:賦值給其他線程中訪問的變量,這個稱之為線程逃逸。

從不逃逸到方法逃逸到線程逃逸,稱之為對象由低到高的不同逃逸程度。

如果確定一個對象不會逃逸出線程之外,那么讓對象在棧上分配內存可以提高 JVM的效率。

 

逃逸分析代碼

 

 

 

 

這段代碼在調用的過程中 Myboject這個對象屬於不可逃逸,JVM可以做棧上分配

然后通過開啟和關閉 DoEscapeAnalysis開關觀察不同。

開啟逃逸分析(JVM默認開啟)

查看執行速度

關閉逃逸分析

 

 

查看執行速度

 

 

 

測試結果可見,開啟逃逸分析對代碼的執行性能有很大的影響!那為什么有這個影響?

 

逃逸分析

如果是逃逸分析出來的對象可以在棧上分配的話,那么該對象的生命周期就跟隨線程了,就不需要垃圾回收,如果是頻繁的調用此方法則可以得到很大的性能提高。

采用了逃逸分析后,滿足逃逸的對象在棧上分配

 

 

 

 

沒有開啟逃逸分析,對象都在堆上分配,會頻繁觸發垃圾回收(垃圾回收會影響系統性能),導致代碼運行慢

 

 

 

 

代碼驗證

開啟 GC打印日志

-XX:+PrintGC

開啟逃逸分析

 

 

 

 

看到沒有 GC日志關閉逃逸分析

 

 

 

 

可以看到關閉了逃逸分析JVM在頻繁的進行垃圾回收GC),正是這塊的操作導致性能有較大的差別

對象優先在 Eden 區分配

大多數情況下,對象在新生代 Eden 區中分配。當 Eden 區沒有足夠空間分配時,虛擬機將發起一次 Minor GC。

 

大對象直接進入老年代

大對象就是指需要大量連續內存空間的 Java 對象,最典型的大對象便是那種很長的字符串,或者元素數量很龐大的數組。

大對象對虛擬機的內存分配來說就是一個不折不扣的壞消息,比遇到一個大對象更加壞的消息就是遇到- -群“朝生夕滅”的“短命大對象”,我們寫程序的時候應注意避免。

在 Java虛擬機中要避免大對象的原因是,在分配空間時,它容易導致內存明明還有不少空間時就提前觸發垃圾收集,以獲取足夠的連續空間才能安置好它們。

而當復制對象時,大對象就意味着高額的內存復制開銷。

HotSpot 虛擬機提供了-XX:PretenureSizeThreshold 參數,指定大於該設置值的對象直接在老年代分配,這樣做的目的就是避免在 Eden 區及兩個 Survivor區之間來回復制,產生大量的內存復制操作。

這樣做的目的:1.避免大量內存復制,2.避免提前進行垃圾回收,明明內存有空間進行分配。

PretenureSizeThreshold 參數只對 Serial  ParNew 兩款收集器有效。-XX:PretenureSizeThreshold=4m

長期存活對象進入老年區

HotSpot 虛擬機中多數收集器都采用了分代收集來管理堆內存,那內存回收時就必須能決策哪些存活對象應當放在新生代,哪些存活對象放在老年代中。

為做到這點,虛擬機給每個對象定義了一個對象年齡(Age)計數器,存儲在對象頭中。

 

 

 

 

 

如果對象在 Eden 出生並經過第一次 Minor GC 后仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將對象年齡設為 1,對象在 Survivor區中每熬過一次 Minor GC,年齡就增加 1,當它的年齡增加到一定程度(並發的垃圾回收器默認為 15),CMS  6 ,就會被晉升到老年代中。

-XX:MaxTenuringThreshold 調整

 

對象年齡動態判定

為了能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到 MaxTenuringThreshold 中要求的年齡

 

空間分配擔保

在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那么 Minor GC 可以確保是安全的。如果不成立,則虛擬機會查看 HandlePromotionFailure 設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次 Minor GC,盡管這次 Minor GC 是有風險的,如果擔保失敗則會進行一次 Full GC;如果小於,或者 HandlePromotionFailure 設置不允許冒險,那這時也要改為進行一次 Full GC。

 

本地線程分配緩沖(TLAB)

 


免責聲明!

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



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