二十、理解對象與垃圾回收機制

一、虛擬機中的對象

1、對象的創建

我們知道在類加載中經歷了加載、驗證、準備、解析、初始化、使用、卸載幾個階段。



在初始化階段中當JVM遇到了一條new指令會經歷以下幾個階段:
(1)檢查加載
檢查類是否被加載,如果加載失敗重新加載
(2)分配內存
爲新生對象分配內存,其實就是在堆空間中劃分一塊確定的連續內存區域
(3)內存空間的初始化
爲對象分配零值,即默認值。例如int的0,boolean的false。
(4)設置
爲對象的請求頭設置信息如對象的哈希碼、在JVM中的分代年齡、class類信息等
(5)對象的初始化
從Java 程序的角度來說,對象已經創建完成了,從開發者的角度纔剛開始,接着就是會調用我們的構造方法完成對象成員變量的賦值。

2、對象的佈局結構


一個對象主要包含了對象頭、實例數據、對齊填充。

3、對象的訪問

我們的Java程序主要是通過棧上的引用來訪問操作具體的對象,主要方式就是通過直接指針的方式,引用中存儲的就是對象的內存地址,通過引用中的地址直接指向堆中的對象。

4、對象的內存分配策略

我們知道幾乎所有的對象都是在堆上分配,說明了也有部分對象不是在堆上分配,而是在棧上分配。

4.1、棧上分配

滿足逃逸的對象會在棧上分配。

逃逸分析原理

我們的對象可能通過作爲參數調用其他的方法,或者被其他線程共享訪問。這種現象我們稱之爲方法逃逸和線程逃逸。當如果確定了我們的對象不會逃逸到線程之外,那麼我們的對象在棧上分配效率會更高。對象的生命週期跟隨着我們的線程,也不需要GC進行回收。如下代碼
在allocate中只是簡單的創建了MyObject 對象,並沒有做其他操作。當方法執行完畢之後,對象的就銷燬。這種情況對象就是在棧上分配。

public class EscapeAnalysisTest {
   public static void main(String[] args) throws Exception {
       long start = System.currentTimeMillis();
       for (int i = 0; i < 50000000; i++) {
           allocate();
      }
       System.out.println((System.currentTimeMillis() - start) + " ms");
       Thread.sleep(600000);
  }

   static void allocate() {
       MyObject myObject = new MyObject(2020, 2020.6);
  }

4.2、堆上分配

以上說滿足逃逸分析的對象會在棧上分配內存,而幾乎所有的對象是在堆中分配。而在堆中又劃分了不同的區域。主要分爲了新生代和老年代,而新生代中又劃分了Eden區、From區、To區。From區和To區稱爲Survivor區。

4.2.1、堆上分配策略

(1)絕大多數的新生對象優先在Eden區分配,當Eden區沒有最夠空間分配的時候虛擬機將發生一次Minor GC。而如果是大對象直接進入老年代,例如很長的字符串以及數組。
(2)長期存活的對象進入老年代,當對象在Eden區發生了一次Minor GC之後仍然存活下來,並且Survivor區域任然可以容納的話,則將對象移動到 Survivor區,並將對象的年齡置爲1,對象在Survivor區每熬過一次Minor GC對象的年齡就+1。當年齡達到一定的程度(一般的回收器是15,CMS是6)的時候就將這些對象移動到老年代。
(3)除了對年齡判斷之外,當Survivor區相同年齡的對象總的大小超過了Survivor區的內存一半,則將大於或者等於該年齡的對象移動到老年代,不需要等待到對象的指定年齡。稱爲對象年齡動態判定。
(4)在發生Minor GC之前,虛擬機會先檢查老年代的最大可用空間是否大於新生代所有對象的大小。如果大於,那麼確保了此次Minor GC是安全的。如果不滿足條件,則虛擬機檢查HandlePromotionFailure是否設置了允許擔保失敗。如果允許,那麼檢查老年代最大可用空間是否大於歷屆從新生代晉升到老年代對象大小的平均值。如果大於,則嘗試進行Minor GC,當然此次GC是有風險的,如果擔保失敗,則會再進行一次Full GC清理堆,棧,方法區等。如果不大於的話,也會發生一次Full GC。

5、判斷對象是否爲垃圾

我們知道堆空間是有限的,當不同的區域空間不足的時候會發生不同類型的GC,GC的目的就是回收掉不需要的對象,我們稱之爲垃圾。要回收垃圾之前,JVM要判斷哪些對象爲垃圾。主要包含兩種算法。

5.1、引用計數算法

在對象中添加一個引用計數器,當有一個地方引用時計數器就+1,引用失效時計數器-1。計數器的結果是0的時候,說明沒有引用,視該對象爲垃圾。但是當對象中包含相互引用的情況下,該方法失效。


5.2、可達性分析算法

該算法的思路是通過GC Roots作爲起點,往下開始搜索,搜索走過的路徑稱爲引用鏈,當一個對象通過引用鏈無法相連的時候,說明該對象是不可用的,應該視爲垃圾。
GC Roots對象主要包含了一下幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。
  • JVM的內部引用(class對象、異常對象NullPointException、OutofMemoryError,系統類加載器)。
  • 所有被同步鎖(synchronized關鍵)持有的對象。
  • JVM內部的JMXBean、JVMTI中註冊的回調、本地代碼緩存等
  • JVM實現中的“臨時性”對象,跨代引用的對象(在使用分代模型回收只回收部分代時)
    如下圖:



    上圖可以通過GC Roots相連的對象我們當之爲應該存活對象,而無法相連的對象我們視爲垃圾。

6、垃圾回收算法

當判斷了對象爲垃圾之後,JVM就通過垃圾回收算法進行回收。主要的垃圾回收算法包括如下

6.1、複製算法

將內存區域分爲兩部分相同的區域,一部分是空的,一部分用來存對象。當發生GC的時候,將存活的對象複製到另外一塊內存區域,然後清理掉原來的區域。優點就是保持了內存的連續性,而缺點就是需要浪費一塊內存,並且對對象進行復制移動,如果存活對象比較多,則性能比較低。
在新生代中通常存活的對象比較少,一般就是採用了複製算法,不需要複製移動太多對象,而在新生代中不是將整個新生代劃分成兩半,而是將新生代劃分成了Eden區,From區、To區。它們的比例通常是8:1:1。而From區和To區稱之爲Survivor區,所以是將Survivor區一分爲二。因爲研究表明百分之98的對象都是朝生夕死的。這樣對新生代的劃分,可以提高JVM堆空間的利用率。所以當Eden區發生MinorGC的時候,將Eden區以及Survivor區的對象複製到Survivor中的空閒部分,例如From區,然後清理掉其他區域。下一次Minor GC的時候,又將Eden區和From區存活的對象複製到To區,再清理掉其他的區域。


6.2、標記清除算法

通過可達性分析算法標記可達的對象,不可達的對象則是需要回收的對象,然後清除。優點就是不需要浪費內存,缺點就是會產生內存碎片。導致內存空間不連續。
一般使用在老年代中,因爲老年代中的對象一般都是比較難回收的,所以需要回收的對象比較少,因此清理少部分對象,不會造成很多內存碎片。

6.3、標記整理算法

標記所有需要回收的對象,將存活的對象移動到了另外一端,將外界的對象直接清理。雖然沒有內存碎片,但是效率比較低,一般也是用在老年代。

二、JVM中常見的垃圾回收器

1、分代收集的思想

  • 在新生代中,每次垃圾收集的時候都發現有大量的對象死去,少部分的對象存活。因此使用複製算法,只需要複製移動極少部分對象,然後清理掉剩餘垃圾對象。
  • 而在老年代中對象的存活率比較高,沒有額外的空間擔保,因此一般採用標記清除和標記整理算法。因爲存活的對象比較多,而垃圾比較少,所以只需標記清理少部分對象。
  • 以上說的存活對象指的是非垃圾對象,死去對象則是通過可達性分析算法分析出的垃圾對象。

2、常見垃圾回收器

Serial/Serial Odl

新生代和老年代的單線程串行收集器

ParNew、ParallelScavenge/Perallel Old

新生代和老年代多線程並行收集器

CMS

多線程併發收集器,主要用於老年代,基於標記清除算法。
垃圾回收器工作的時候所有的用戶線程會停掉,這就是所說的Stop The Worl現場,如果用戶線程停止太久,那麼就會造成用戶體驗卡頓。併發收集器則指的是垃圾回收線程和用戶線程併發執行。
但是在CMS進行垃圾回收的時候也不是所有的階段都和用戶線程併發執行。如下圖:


(1)初始標記
這個階段只是通過可達性分析算法標記GC Roots直接關聯的對象,這部分對象比較少。速度很快。這個階段暫停了用戶線程。
(2)併發標記
標記GC Roots所有關聯的對象,花費時間比較久,用戶線程和GC線程同時執行
(3) 重新標記
修正在併發標記中用戶線程運行有可能產生的垃圾對象,所以需要重新標記。這個階段時間也比較短,暫停了用戶線程。
(4) 併發清除
標記完之後清除垃圾,用戶線程和GC線程併發工作。

浮動垃圾

在CMS工作中,在併發標記和併發清除階段GC線程和用戶線程併發工作,因此避免不了這個過程用戶線程產生了新的垃圾,這個垃圾稱之爲浮動垃圾。下一次GC的時候進行回收。

3、Stop The Worl現象

當垃圾回收器開始工作進行垃圾回收的時候,所有的用戶線程會停掉,只有垃圾回收線程在工作。這種現場稱之爲Stop The World。因此頻繁的垃圾回收會造成頻繁的Stop The World。我們的用戶線程頻繁的停掉和恢復,這樣就會造成應用的卡頓。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章