您當前的位置:首頁 > 收藏

JVM 垃圾回收

作者:由 老閆師傅 發表于 收藏時間:2017-04-16

在寒假時,看完了周志明老師的《深入理解JAVA虛擬機器》,書中有專門的一章來講解垃圾回收,又在一次面試時被問到,感覺垃圾回收是JVM的一個很重要的機制,所以打算專門寫一篇部落格。

要想搞明白垃圾回收,就是要搞明白

四個問題

在JVM的哪塊記憶體中發生垃圾回收?

哪些物件需要被回收?

什麼時候回收?

怎樣回收這些物件?

首先是第一個問題:在JVM的哪塊記憶體中發生垃圾回收?

這裡,我們要先說一下JVM的記憶體分割槽。

1。程式計數器:當前執行緒所執行的位元組碼的行號指示器,控制著程式的分支、迴圈、跳轉、異常處理。是執行緒私有的。

2。虛擬機器棧:為每個方法在執行時建立一個棧幀,用於儲存區域性變量表、運算元棧棧等。其中區域性變量表儲存基本資料型別和

物件引用

。是執行緒私有的。

3。JAVA堆:所有

物件例項

都在堆上分配。所有執行緒共享此塊記憶體。

4。方法區:所有執行緒共享的記憶體區域,用於儲存虛擬機器載入的類資訊、常量、靜態變數等。

看完上面對於JVM記憶體的各個區域介紹,應該已經知道了垃圾回收發生在哪塊區域了。由於垃圾回收是回收和物件有關的東西,所以主要發生在虛擬機器棧和堆中;少部分發生在方法區。

下圖更詳細的說明了垃圾回收發生的位置:

本地方法棧與虛擬機器棧相似,經常在一些虛擬機器實現中被合併起來。

JVM 垃圾回收

JVM 垃圾回收

下來是第二個問題,哪些物件需要被回收?

其實這個問題很好回答,當然是不用的物件需要被回收啦!但是JVM不知道哪些物件是你不用的。所以就有了另一種判斷方法:不能用的物件就是你不需要用的,因為你就算想用也用不到,所以必然是不用的物件。

所以就有了引用計數法來判斷一個物件是否可以使用。

引用計數法非常簡單——給一個物件新增一個引用計數器,初始值為零,有一個地方引用它時,計數器加一,當一個引用失效時,計數器減一。計數器為零時此物件不可用。

下面就是對一個引用計數的舉例:

JVM 垃圾回收

JVM 垃圾回收

引用計數法有一個致命的缺陷,如下圖,就是當兩個物件相互引用時,這兩個物件實際是不可獲得的,但是由於引用計數不為零,所以均不會被回收。

JVM 垃圾回收

JVM 垃圾回收

為了解決這個問題,就有了可達性分析法:

演算法的思路是透過一系列稱為GC Roots的物件作為起始點,從這些節點向下搜尋,當一個物件到GC Roots時沒有任何引用鏈相連,證明此物件是不可用的。如下圖,從GC Root不能到達ObjD ObjF ObjE,所以這三個物件是不可用的。

可做GC Roots的物件有 虛擬機器棧中引用的物件(本地變量表)、方法區中靜態屬性引用的物件、方法區中常量引用的物件、本地方法棧中引用的物件(Native物件)。

JVM 垃圾回收

JVM 垃圾回收

接下來要說說JVM中的四種引用:

1。強引用:程式中普遍存在的一種引用 Object o = new Object();垃圾收集器永遠不會回收被強引用的物件。

2。軟引用:如下所示,當我們存在有用但不是必需的物件時,例如快取,就可以使用軟引用。

只要記憶體空間足夠,軟引用物件就不會被回收,將要發生記憶體溢位異常時,會將軟引用的物件回收。

SoftReference sr = new SoftReference(new String(“hello”));

3。弱引用:弱引用也是用來描述非必需物件的,當JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的物件。

WeakReference sr = new WeakReference(new String(“hello”));

4。虛引用:虛引用和前面的軟引用、弱引用不同,它並不影響物件的生命週期。在java中用java。lang。ref。PhantomReference類表示。如果一個物件與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。

要注意的是,虛引用必須和引用佇列關聯使用,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用佇列中。程式可以透過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。如果程式發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。

ReferenceQueue

<

String

>

queue

=

new

ReferenceQueue

<

String

>();

PhantomReference

<

String

>

pr

=

new

PhantomReference

<

String

>(

new

String

“hello”

),

queue

);

下面是第三個問題:什麼時候發生垃圾回收?(此處我說的不夠準確,可以看一下大神的回答 :Major GC和Full GC的區別是什麼?觸發條件呢?)

首先需要知道,GC又分為minor GC 和 Full Gc(也稱為Major GC)。Java 堆記憶體分為新生代和老年代,新生代中又分為1個Eden區域 和兩個 Survivor區域。

那麼對於 Minor GC 的觸發條件:

大多數情況下,直接在 Eden 區中進行分配

。如果 Eden區域沒有足夠的空間,那麼就會發起一次 Minor GC;對於 Full GC(Major GC)的觸發條件:也是如果老年代沒有足夠空間的話,那麼就會進行一次 Full GC。

實際上,需要考慮一個空間分配擔保的問題

在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件的總空間。如果大於則進行Minor GC,如果小於則看HandlePromotionFailure設定是否允許擔保失敗(不允許則直接Full GC)。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於則嘗試Minor GC(如果嘗試失敗也會觸發Full GC),如果小於則進行Full GC。

接下來是最後一個問題:怎樣進行垃圾回收?

這一部分主要說說垃圾回收的幾種演算法思路。

標記-清除演算法:先標記出需要被回收的物件,然後全部清除,如下圖所示:

這種演算法有兩個嚴重的問題,

一是標記清楚的效率不高,二是產生記憶體碎片

JVM 垃圾回收

JVM 垃圾回收

為了解決效率低的問題,有了

複製演算法

:將記憶體劃分為相等的兩塊區域A和B,一次只用其中一塊A,當需要垃圾回收時,將A中所有存活的物件複製到B,然後清楚A,使用B。就這樣週而復始。但是也有一個明顯的問題:

可使用的記憶體大小隻有一半。#FormatImgID_11##FormatImgID_12#

為了解決記憶體碎片問題,又有了

標記整理演算法

:先標記,然後讓所有存活物件向另一端移動,然後直接清理端邊界以外的記憶體。如下圖:

JVM 垃圾回收

JVM 垃圾回收

我們將以上兩種方法結合起來,就可以得到一種較為兩全的方法——

分代收集法:

這裡再說一遍上面提到過的老年代和新生代。

新生代:

所有新物件建立發生在Eden區,Eden區滿後觸發新生代上的minor GC,將Eden區和非空閒Survivor區存活物件複製到另一個空閒的Survivor區中。

永遠保證一個Survivor是空的,新生代minor GC就是在兩個Survivor區之間相互複製存活物件,直到Survivor區滿為止。

由於新生代大多數物件是

“朝生夕死”

的所以,對於新生代採用有兩個很小的Survivor區、一個大的Eden區,使用複製演算法的原理進行回收:一次使用一個Survivor區1和Eden區,生還的物件移入另一個保留區2,然後清空所有,週而復始。

JVM 垃圾回收

JVM 垃圾回收

老年代:

Eden區滿後觸發minor GC將存活物件複製到Survivor區,Survivor區滿後觸發minor GC將存活物件複製到老年代。

經過新生代的兩個Survivor之間多次複製,仍然存活下來的物件就是年齡相對比較老的,就可以放入到老年代了,隨著時間推移,如果老年代也滿了,將觸發Full GC,針對整個堆(包括新生代、老年代)進行垃圾回收。

老年代大多數物件會長期存活,不適合複製演算法,所以使用標記-整理演算法。

到這,上面所說的四個問題就全部回答完了。

下面還有兩個小問題:

1。為什麼C/C++沒有垃圾回收機制?

因為C/C++可以直接操控記憶體地址,以至於不能判斷出哪個物件是可用或不可用,因為每個物件都是可以透過記憶體直接獲得的。

2。避免FullCG。

將轉移到老年代的物件數量降到最少。

可以透過增加新生代空間的大小來減少進入老年代的物件數量。

減少Full GC的執行時間。

如果你試圖透過消減老年代空間來減少Full GC的執行時間,可能會導致OutOfMemoryError 或者 Full GC執行的次數會增加。與之相反,如果你試圖透過增加老年代空間來減少Full GC執行次數,執行時間會增加。

所以需要將老年代設定為一個合適的值。

這些就是我對JVM中垃圾回收的理解,不足之處希望指出。

標簽: GC  物件  引用  回收  垃圾