JVM垃圾回收機制(Garbage Collection)

NAOKO發表於2017-12-21

1. 概述

Java記憶體區域裡講了Java的記憶體執行時資料區域分為如下5個部分

  • 程式計數器(Program Counter)
  • 虛擬機器棧(Virtual Machine Stack)
  • 本地方法棧(Native Method Stack)
  • 堆(Heap)
  • 方法區(Method Area)

其中前三個資料區域隨著執行緒的啟動而建立,終止而銷燬,這三個區域的記憶體回收具有確定性,不需要過多考慮回收問題。所以JVM的垃圾回收機制的注意力就集中於堆和方法區,其中對堆的GC價效比是最高的,一般可以回收70%~95%的空間。

2. GC過程

首先討論的是對堆的GC,在這之前我們應該知道要進行垃圾回收的步驟應該是

  • 知道哪些物件需要回收?
  • 用什麼方式去回收?

判斷物件的存活

針對第一個問題我們得確定堆中物件的“存活”,一個物件的“存活”其實就是能否通過任何途徑使用該物件,下面通過一段Code看下就明白:

public class Main{
	public static void main(String[] args){
		A a = new A();
		a = null;
	}
}
複製程式碼

在這段Code裡面,一開始建立一個A型別的物件,變數a持有這個物件的引用,接著a被賦值為null後。從此就無法通過任何變數來使用這個物件了,那麼這個物件也就是所謂的“死亡”了,而GC的 就是這些物件。接下來有兩種方法可以找出堆中存活和死亡的物件。

引用計數法(Reference Counting)

給每一個物件新增一個引用計數器,每當物件被引用,就對該物件的引用計數器加一,當引用失效時引用計數器就減一。直到物件的引用計數器為0時該物件就是已死亡,可被GC。這種方法看起來簡單高效,但JVM卻沒有使用它來判斷物件的存活,原因是它很難解決物件之間相互引用的問題。還是來一段Code看下:

public class Main{
	public static void main(String[] args){
		A a = new A();
		B b = new B();

		a.ref = b;
		b.ref = a;

		a = null;
		b = null;
	}
}
複製程式碼

在這段Code中,ab兩個引用最後都null,也就是無法通過它們來使用一開始建立的兩個物件,雖然這樣它們卻無法回收,原因是建立的兩個物件相互引用導致兩個物件的引用計數器都不為0。所以也就有了第二種方法**(可達性分析)**來解決這個問題。

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

把堆中所有物件當成一幅有向圖中的所有點,物件之間的引用構成了點與點的之間的路徑。接著從一系列被稱為GC Roots(一些被引用的物件)的點出發遍歷整個圖,圖中所有可以到達的點都是存活的物件,而那些不可到達的點則為死亡物件,將被GC。 可充當GC Roots的物件有下面幾種:

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

垃圾回收演算法

解決完第一個問題(判斷物件的存活)後,就可以去回收這些物件佔用的記憶體了,至於怎麼回收這些記憶體,有下面幾種演算法:

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

標記-清除演算法如同它的名字一樣,有標記和清除兩個階段。其中的標記階段就是上面說到的確定物件的存活階段,確定了要回收的物件後就回收死亡的物件,存活的物件留在原地。標記清除演算法是最基礎的回收演算法,它有兩個缺點:

  • 標記和清除階段效率都不高
  • 清除之後記憶體會產生大量不連續的碎片,導致分配大記憶體物件困難
    標記清除演算法

複製演算法(Copying)

複製演算法將記憶體分為大小相等的兩塊,每次只使用一塊,待這塊記憶體用完,將這一塊上存活的物件複製到另一塊上,再把存在垃圾物件的那一塊佔用的記憶體一次清掉。這樣做效率高的原因是存活的物件遠遠少於死亡的物件,從而只需複製少量的存活物件。

複製演算法

複製演算法解決了標記-清除演算法的清除階段效率低的問題和碎片問題但卻使可用記憶體減少一半。其實有個辦法可以解決這個問題:

IBM公司的專業研究表明新生代中的物件98%是“朝生夕死”的,所以並不用按照1:1來劃分空間,而是將記憶體分為3塊。一塊80%大小的Eden空間和兩塊10%大小的Survivor空間,每次使用一塊Eden和一塊Survivor,當需要回收時,將使用中的Eden和Survivor上的存活物件複製到另一塊Survivor上,最後直接清理使用過的Eden和Survivor的記憶體空間。這樣就使得空間的利用率達到90%。但如果存活的物件超過10%的話,Survivor的空間就不夠用了,這時就需要依賴老年代進行分配擔保。

標記整理演算法(Mark-Compact)

相比於複製演算法,標記整理演算法使用與適用於老年代這種物件存活率高的區域。標記整理和標記清除很相似,前面的標記步驟都一樣,不一樣在標記整理在清除前多做了整理步驟讓存活的物件向一端移動,最後在清除掉端邊界以外的記憶體。

標記整理演算法

分代收集演算法(Generational Collection)

因為現在的商用JVM的垃圾回收都採用分代收集演算法,所以一般把堆記憶體劃分為新生代和老年代。剛建立的物件存在於新生代中,當有一些物件經歷垃圾回收達到一定次數還存活下來的話,這些物件將進入老年代,所以老年代裡的物件每次GC存活率都很高。因此針對於新生代和老年代物件的不同存活率,可以分別採取不同的垃圾回收演算法,對於物件存活率低的新生代採用複製演算法,而對於物件存活率高的老年代採用標記清除或標記整理演算法。

以上介紹的是關於堆中的GC,下面來說下方法區的GC。


方法區的GC

方法區在HotSpot虛擬機器中是永久代,相比於堆中的新生代和老年代,永久代進行垃圾回收的價效比更低。 方法區的垃圾回收主要回收廢棄常量和無用的類,其中常量來自於方法區的常量池,包括字面值常量和符號引用。回收常量跟回收堆中物件非常類似,以字面值常量為例,如果不存在其他物件引用該字面值常量,如果發生GC且有必要的話,該字面值常量會被回收。對於無用的類的判斷比較苛刻,必須同時滿足下列三個條件:

  • 該類的所以例項都被回收
  • 載入該類的類載入器已經被回收
  • 該類對應的Class物件沒有在任何地方被引用

不過也可以滿足了上面的三個條件也不進行回收,可以通過設定虛擬機器引數來控制回收。

相關文章