在判斷哪些內存需要回收和什么時候回收用到GC 算法,本文主要對GC 算法進行講解。
JVM垃圾判定算法
常見的JVM垃圾判定算法包括:引用計數算法、可達性分析算法。
引用計數算法(Reference Counting)
引用計數算法是通過判斷對象的引用數量來決定對象是否可以被回收。
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。
優點:簡單,高效,現在的objective-c用的就是這種算法。
缺點:很難處理循環引用,相互引用的兩個對象則無法釋放。因此目前主流的Java虛擬機都摒棄掉了這種算法。
舉個簡單的例子,對象objA和objB都有字段instance,賦值令objA.instance=objB及objB.instance=objA,除此之外,這兩個對象沒有任何引用,實際上這兩個對象已經不可能再被訪問,但是因為互相引用,導致它們的引用計數都不為0,因此引用計數算法無法通知GC收集器回收它們。
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();//GC
}
}
運行結果
[GC (System.gc()) [PSYoungGen: 3329K->744K(38400K)] 3329K->752K(125952K), 0.0341414 secs] [Times: user=0.00 sys=0.00, real=0.06 secs]
[Full GC (System.gc()) [PSYoungGen: 744K->0K(38400K)] [ParOldGen: 8K->628K(87552K)] 752K->628K(125952K), [Metaspace: 3450K->3450K(1056768K)], 0.0060728 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 998K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000)
eden space 33280K, 3% used [0x00000000d5c00000,0x00000000d5cf9b20,0x00000000d7c80000)
from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000)
to space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000)
ParOldGen total 87552K, used 628K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000)
object space 87552K, 0% used [0x0000000081400000,0x000000008149d2c8,0x0000000086980000)
Metaspace used 3469K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 381K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
從運行結果看,GC日志中包含“3329K->744K”,意味着虛擬機並沒有因為這兩個對象互相引用就不回收它們,說明虛擬機不是通過引用技術算法來判斷對象是否存活的。
可達性分析算法(根搜索算法)
可達性分析算法是通過判斷對象的引用鏈是否可達來決定對象是否可以被回收。
從GC Roots(每種具體實現對GC Roots有不同的定義)作為起點,向下搜索它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。
在Java語言中,可以作為GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中的本地變量表)中的引用對象。
- 方法區中的類靜態屬性引用的對象。
- 方法區中的常量引用的對象。
- 本地方法棧中JNI(Native方法)的引用對象
真正標記以為對象為可回收狀態至少要標記兩次。
四種引用
強引用就是指在程序代碼之中普遍存在的,類似"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
Object obj = new Object();
軟引用是用來描述一些還有用但並非必需的對象,對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。在JDK1.2之后,提供了SoftReference類來實現軟引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象,只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK1.2之后,提供了WeakReference類來實現弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
虛引用也成為幽靈引用或者幻影引用,它是最弱的一中引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2之后,提供給了PhantomReference類來實現虛引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
JVM垃圾回收算法
常見的垃圾回收算法包括:標記-清除算法,復制算法,標記-整理算法,分代收集算法。
在介紹JVM垃圾回收算法前,先介紹一個概念。
Stop-the-World
Stop-the-world意味着 JVM由於要執行GC而停止了應用程序的執行,並且這種情形會在任何一種GC算法中發生。當Stop-the-world發生時,除了GC所需的線程以外,所有線程都處於等待狀態直到GC任務完成。事實上,GC優化很多時候就是指減少Stop-the-world發生的時間,從而使系統具有高吞吐 、低停頓的特點。
標記—清除算法(Mark-Sweep)
之所以說標記/清除算法是幾種GC算法中最基礎的算法,是因為后續的收集算法都是基於這種思路並對其不足進行改進而得到的。標記/清除算法的基本思想就跟它的名字一樣,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
標記階段:標記的過程其實就是前面介紹的可達性分析算法的過程,遍歷所有的GC Roots對象,對從GC Roots對象可達的對象都打上一個標識,一般是在對象的header中,將其記錄為可達對象;
清除階段:清除的過程是對堆內存進行遍歷,如果發現某個對象沒有被標記為可達對象(通過讀取對象header信息),則將其回收。
不足:
- 標記和清除過程效率都不高
- 會產生大量碎片,內存碎片過多可能導致無法給大對象分配內存。
復制算法(Copying)
將內存划分為大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了就將還存活的對象復制到另一塊上面,然后再把使用過的內存空間進行一次清理。
現在的商業虛擬機都采用這種收集算法來回收新生代,但是並不是將內存划分為大小相等的兩塊,而是分為一塊較大的 Eden 空間和兩塊較小的 Survior 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象一次性復制到另一塊 Survivor 空間上,最后清理 Eden 和 使用過的那一塊 Survivor。HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認為 8:1,保證了內存的利用率達到 90 %。如果每次回收有多於 10% 的對象存活,那么一塊 Survivor 空間就不夠用了,此時需要依賴於老年代進行分配擔保,也就是借用老年代的空間。
不足:
- 將內存縮小為原來的一半,浪費了一半的內存空間,代價太高;如果不想浪費一半的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
- 復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。
標記—整理算法(Mark-Compact)
標記—整理算法和標記—清除算法一樣,但是標記—整理算法不是把存活對象復制到另一塊內存,而是把存活對象往內存的一端移動,然后直接回收邊界以外的內存,因此其不會產生內存碎片。標記—整理算法提高了內存的利用率,並且它適合在收集對象存活時間較長的老年代。
不足:
效率不高,不僅要標記存活對象,還要整理所有存活對象的引用地址,在效率上不如復制算法。
分代收集算法(Generational Collection)
分代回收算法實際上是把復制算法和標記整理法的結合,並不是真正一個新的算法,一般分為:老年代(Old Generation)和新生代(Young Generation),老年代就是很少垃圾需要進行回收的,新生代就是有很多的內存空間需要回收,所以不同代就采用不同的回收算法,以此來達到高效的回收算法。
新生代:由於新生代產生很多臨時對象,大量對象需要進行回收,所以采用復制算法是最高效的。
老年代:回收的對象很少,都是經過幾次標記后都不是可回收的狀態轉移到老年代的,所以僅有少量對象需要回收,故采用標記清除或者標記整理算法。