物件回收判定與垃圾回收演算法-JVM學習筆記(1)

拜仁fans發表於2019-06-17

本章要探究的問題 :

GC在回收記憶體時 :

  1. 怎麼判斷哪些記憶體需要回收 ?
  2. 什麼時候回收?

在幾個執行緒私有的執行時區域:

image.png

  • 虛擬機器棧
  • 程式計數器
  • 本地方法棧

它們的記憶體分配和回收大多都具有確定性,隨著執行緒的建立而產生,隨著執行緒的停止而被回收。棧幀中的記憶體大小基本在類的結構確定下來時就已知。

而線上程共有的 Java堆(Heap)方法區(Class(Method) Area) 這兩個區域則不同:

image.png

比如,一個介面有不同的實現類(類的資訊在方法區中),這幾個實現類的記憶體大小肯定不一,沒法在執行前就已知需要多大的記憶體,只有在執行期間才知道建立的物件的大小。


一,哪些記憶體需要回收?

在知道哪些記憶體需要回收之前,我們要知道怎麼判斷一個物件是否還存活,當它不再存活時,就回收它。
引用計數演算法 就是用來判斷物件是否存活的一個演算法。

1,引用計數演算法(Reference Counting)

演算法描述:給物件新增一個引用計數器,當有一個地方引用了它,計數器+1,當引用失效,計數器-1,在任何時刻,計數器為0時此物件將不能再被使用。

引用計數法在大多數情況下表現都不錯,也有被很多公司採用的應用案例。但是在JVM中並沒有採用這種演算法,原因是:無法解決物件之間存在相互引用的問題

public class Person {
    Object instance = null;

    public static void main(String[] args) {
        Person a = new Person();
        Person b = new Person();
        
        a.instance = b;
        b.instance = a;
        
        a = null;
        b = null;// 正常情況下在這裡GC就會把a,b回收掉
    }
}
複製程式碼

正常情況下在執行11-12行程式碼時,JVM的GC會把a,b兩個物件回收,但是在引用計數演算法的情況下:

  • 執行 a=null 時,a的引用計數器值為1,因為b物件在引用它。
  • 執行 b=null 時,b的引用計數器值為1,因為a物件在引用它。

2,可達性分析(Reachability Analysis)演算法

在Java語言中是通過可達性分析來判斷物件是否存活。
演算法描述 : 通過一系列的 GC Roots 作為起始點,從這些起始點開始向下搜尋,能搜尋的到的物件說明其可用,不會被GC回收掉,搜尋所走過的路徑稱為 引用鏈(Reference Chain) 。相反,如果一個物件沒有到達GC Roots的路徑,則說明它不可用,被判定為可被GC回收的物件。

image.png

如圖 : 1區域的物件雖然互相關聯,但是它們不可到達GC Roots,所以他們會被回收掉,而2區域的物件與GC Roots之間是有可到達路徑的,所以它們不會被回收。

什麼是GC Roots ?

  • 虛擬機器棧(棧幀中的本地變數表)中引用的物件
  • 方法區中類的靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中JNI(Native方法)引用的物件

這些都可作為GC Roots.

3,什麼是引用(Reference)

我們在上面的 引用計數演算法可達性分析 中,都提到了 物件之間的引用 關係。

在Java1.2之前,關於 引用 的定義 :

如果 reference 型別的資料儲存的數值代表的是另一塊記憶體的起始地址,就說這塊記憶體代表一個引用。

JDK1,2之後,又引入了 強引用軟引用弱引用虛引用 ,這四個概念,並且這四種表現的引用關係越來越弱。

  • 強引用(Strong Reference) :

例:

Object o = new Object();
複製程式碼


只要強引用還在,GC永遠不會回收掉被引用的物件。

  • 軟引用(Soft Reference) :

有用,但非必須,在將要發生記憶體溢位時,會把 軟引用 的物件回收掉,如果記憶體依然不夠用,則丟擲OOM異常。

  • 弱引用(Weak Reference):

非必需物件,只要GC發生了垃圾回收,不管此時記憶體是否充足, 弱引用 的物件都會被回收掉。

  • 虛引用(Phantom Reference):
    • 最弱的引用關係
    • 無法通過虛引用構造市例項。
    • 唯一的作用就是在虛引用關聯的物件被GC回收掉時,可以接受到一個訊號。

4,如何判斷一個物件可回收(已死)?

一個物件僅僅通過上面說的可達性分析看它沒有與GC ROOTS關聯來判定這個物件是否可被回收是不夠的。

一個物件要經過下面一段判斷過程來判斷它是否要被回收(建議收藏(^__^) 嘻嘻……):

image.png

5,方法區的回收

上面我們說的是存在於Java堆中的物件的回收,但其實在方法區還要回收以下東西:

① 回收廢棄常量

假如常量池中有一個字串 "abc" ,但是系統中沒有一個String 物件指向它,也就是這個常量沒有被引用,
當GC在回收時會回收此字面量。

② 回收廢棄的類(無用的類)

  1. 該類的例項都已被回收,Java堆中不存在任何該類的例項。
  2. 載入該類的ClassLoader已經被回收。
  3. 該類的Java.lang.class物件沒有被引用(在反射中會被用到這點)。

③ 方法區的回收策略:

GC在回收方法區時會採用一下2種方式:

  • 標記-整理
  • 標記-清除


二,如何回收?

GC在回收記憶體時會採用多種垃圾收集演算法,這些演算法各有優劣。

1.標記-清除(Mark-Sweep)演算法

此演算法是最基礎也是最古老的垃圾回收演算法,該演算法主要經過2個過程

① 演算法描述

  1. 標記階段:經過如何判斷一個物件可被回收所述,對可被回收的物件進行標記。
  2. 清除階段:將被標記的物件統一回收。

物件回收判定與垃圾回收演算法-JVM學習筆記(1)

② 演算法缺陷

  1. 效率問題:此種演算法標記和清除的效率都不高。
  2. 標記清除後產生大量不連續的記憶體空間碎片。

2.複製(Copying)演算法

複製演算法針對效率問題進行了優化,它將記憶體區域劃分為2塊,每次只使用其中一塊。

  • 活動區域
  • 空閒區域

① 演算法描述

image.png

如圖:

  • 回收前 : 記憶體被劃分為左右兩側區域,右側為空閒區域,暫時不使用它
  • 回收時 : 將左側要被回收的部分(黑色) 回收掉,然後將4個存活物件(淡灰色)移動到右側的空閒區域,並且做了2件事
    • 將移動到空閒區域的存活物件按記憶體地址進行排列。
    • 將存活物件指向的舊地址指向新記憶體地址。
  • 回收後 : 原先的右側空閒區域變為活動區域,左側的活動區域變為空閒區域。

左右兩側的區域狀態在每一次回收後都來回轉換...

② 演算法缺陷

  1. 很顯然,這種演算法浪費了一般記憶體。
  2. 當活動區域的100%的物件都還在活躍,那麼在回收時需要將全部的物件複製到右側的空閒區域,此時的效率就很低。

③ 演算法應用

IBM公司經研究表明,Java堆新生代種的物件98%是 '朝生夕死' 的物件,比如臨時變數等作用域很少的物件。
所以現在的虛擬機器並不會按照 1:1的比例劃分兩個區域。

image.png

現在的JVM虛擬機器中,將新生代劃分為一塊 Eden 區域,和2塊較小的 Survivor 區域(from ,to區)。
每次使用Eden區和1塊Survivor區(from區)最為活動區域,當發生記憶體回收時,將這2塊記憶體中的存活物件複製到另一塊Survivor區(to區)。

HotSpot 虛擬機器中,Eden區和Survivor的劃分是: 8: 1,這樣,活動區域佔新生代的 (8+1)/10 *100% = 90%,只有10%的記憶體浪費。

老年代 : 當將存活物件從活動區域(Eden,from) 複製到 to區時,如果to區不夠用,則將剩下的存活物件放到老年代。

3.標記-整理演算法

標記-整理主要運用於老年代中。

① 演算法描述

image.png

此演算法與標記-清除演算法類似,也是經歷2個階段:

  1. 標記階段:此階段於標記-清除中的標記階段相同,都是標記出要回收的物件。
  2. 整理階段:把所有存活的對像,按記憶體地址排列移動到記憶體區域的一端,將端邊界以外的區域進行回收。

② 演算法應用

由於老年代的特點,物件的存活率較高,沒有額外的空閒區域,所以 老年代適用 標記-清除和標記-整理演算法。








(完~)

Reference:
深入理解Java虛擬機器

image.png

相關文章