深刻理解Java虛擬機:垃圾收集器與內存分配策略

 

3.2 對象已死嗎

判斷一個對象是否可被回收

  • 1.引用計數法

對堆中每一個對象添加一個引用計數器;當對象被引用時,引用計數器加1;當引用被置爲空或離開做用域時,引用計數減1。簡單但效率低,沒法解決相互引用的問題。算法

public class ReferenceCountingGC {

  public Object instance = null;

  public static void main(String[] args) {
    ReferenceCountingGC objectA = new ReferenceCountingGC();
    ReferenceCountingGC objectB = new ReferenceCountingGC();
    objectA.instance = objectB;
    objectB.instance = objectA;
  }
}
  • 2.可達性分析算法(根搜索算法)

利用JVM維護的對象引用圖,從根節點( GC Roots)開始遍歷對象的引用圖,同時標記遍歷到的對象。當遍歷結束後,未被標記的對象就是目前已不被使用的對象,能夠被回收了。在Java語言中,可做爲GC Roots的對象包括下面幾種:數組

  1. 虛擬機棧中引用的對象
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中JNI引用的對象

引用類型

  • 1.強引用

被強引用關聯的對象不會被回收。安全

使用 new 一個新對象的方式來建立強引用。多線程

Object obj = new Object();2.併發

  • 2.軟引用

被軟引用關聯的對象只有在內存不夠的狀況下才會被回收。ide

使用 SoftReference 類來建立軟引用。性能

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使對象只被軟引用關聯
  • 3.弱引用

被弱引用關聯的對象必定會被回收,也就是說它只能存活到下一次垃圾回收發生以前。this

使用 WeakReference 類來實現弱引用。線程

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;  // 使對象只被弱引用關聯
  • 4.虛引用

又稱爲幽靈引用或者幻影引用。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用取得一個對象。

爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被回收時收到一個系統通知。

使用 PhantomReference 來實現虛引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;  // 使對象只被虛引用關聯

finalize()

若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法:

  1. 當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。此對象將被回收。
  2. 若是這個對象被斷定爲有必要執行finalize()方法【重寫了finalize()方法,且該方法沒有被調用過】,那麼這個對象將會放置在一個叫作F-Queue的隊列之中,並在稍後由一個由虛擬機自動創建的、 低優先級的Finalizer線程去執行它。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣作的緣由是,若是一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的狀況),將極可能會致使F-Queue隊列中其餘對象永久處於等待,甚至致使整個內存回收系統崩潰。

finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身——只要從新與引用鏈上的任何一個對象創建關聯便可,譬如把本身(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出「即將回收」的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。

/**
 * 此代碼演示了2點:
 * 1.對象能夠在被GC時自我拯救。
 * 2.這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次
 */
public class FinalizeEscapeGC {

      public static FinalizeEscapeGC SAVE_HOOK = null;

      public void isAlive() {
            System.out.println("yes,i am still alive:)");
      }

      @Override
      protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("finalize mehtod executed!");
            FinalizeEscapeGC.SAVE_HOOK = this;
      }

      public static void main(String[] args) throws InterruptedException {
            SAVE_HOOK = new FinalizeEscapeGC();
            // 對象第一次成功拯救本身
            SAVE_HOOK = null;
            System.gc();
            // 由於finalize方法優先級很低,因此暫停0.5秒以等待它
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                  SAVE_HOOK.isAlive();
            } else {
                  System.out.println("no,i am dead:(");
            }
            // 下面這段代碼與上面的徹底相同,可是此次自救卻失敗了
            SAVE_HOOK = null;
            System.gc();
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                  SAVE_HOOK.isAlive();
            } else {
                  System.out.println("no,i am dead:(");
            }
      }
}

Console:
finalize mehtod executed!
yes,i am still alive:)
no,i am dead:(

回收方法區

永久代的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。類須要同時知足下面3個條件才能算是「無用的類」:

  1. 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
  2. 加載該類的ClassLoader已經被回收。
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

 

3.3. 垃圾收集算法

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

這是最基礎的垃圾回收算法,之因此說它是最基礎的是由於它最容易實現,思想也是最簡單的。標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出全部須要被回收的對象,清除階段就是回收被標記的對象所佔用的空間。

2.Copying(複製)算法

爲了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。

3.Mark-Compact(標記-整理)算法

該算法標記階段和Mark-Sweep同樣,可是在完成標記以後,它不是直接清理可回收對象,而是將存活對象都向一端移動,而後清理掉端邊界之外的內存。雖然能夠大大簡化消除碎片的工做,可是每次處理都會帶來性能的損失。

4.Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集時只有少許對象須要被回收,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。

  • 新生代使用:複製算法
  • 老年代使用:標記 - 清除 或 標記 - 整理 算法

 

3.5 垃圾收集器

1.Serial

Serial/Serial Old收集器是最基本最古老的收集器,它是一個單線程收集器,而且在它進行垃圾收集時,必須暫停全部用戶線程,直到垃圾收集結束。Serial收集器是針對新生代的收集器,採用的是Copying算法,它簡單高效,對於限定單個CPU環境來講,沒有線程交互的開銷,能夠得到最高的單線程垃圾收集效率,所以Serial垃圾收集器依然是java虛擬機運行在Client模式下默認的新生代垃圾收集器。

2.ParNew

ParNew收集器是Serial收集器的多線程版本,也使用Copying算法 ,除了使用多個線程進行垃圾收集外,其他的行爲和Serial收集器徹底同樣。ParNew收集器默認開啓和CPU數目相同的線程數,能夠經過-XX:ParallelGCThreads參數來限制垃圾收集器的線程數。ParNew垃圾收集器是不少java虛擬機運行在Server模式下新生代的默認垃圾收集器。

3.Parallel Scavenge

Parallel Scavenge收集器是一個新生代的多線程收集器(並行收集器),它在回收期間不須要暫停其餘用戶線程,其採用的是Copying算法,該收集器與前兩個收集器有所不一樣,它主要是爲了達到一個可控的吞吐量(吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間))。高吞吐量能夠最高效率地利用CPU時間,儘快地完成程序的運算任務,主要適用於在後臺運算而不須要太多交互的任務。

4.Serial Old

Serial Old收集器是針對老年代的收集器,採用的是Mark-Compact算法。它的優勢是實現簡單高效,可是缺點是會給用戶帶來停頓。在Server模式下,主要有兩個用途:a.在JDK1.5以前版本中與新生代的Parallel Scavenge收集器搭配使用。b.做爲年老代中使用CMS收集器的後備垃圾收集方案。

5.Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(並行收集器),使用多線程的Mark-Compact算法。

6.CMS

CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器,它是一種併發收集器,採用的是Mark-Sweep算法。

7.G1

G1收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多CPU、多核環境。採用Mark-Compact算法,所以它是一款並行與併發收集器,而且它能創建可預測的停頓時間模型。

 

3.6 內存分配與回收策略

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動做,由於Java對象大多都具有朝生夕滅的特性,因此Minor GC很是頻繁,通常回收速度也比較快。
  • 老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。 Major GC的速度通常會比Minor GC慢10倍以上。

內存分配策略

  • 1.對象優先在Eden區分配

對象在新生代Eden區中分配,當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC。

  • 2.大對象直接在老年代中分配

大對象直接在老年代中分配,JVM提供參數設置 –XX:PretenureSizeThreshold,當對象須要的空間大於該值時,直接在老年代分配(目的:避免在Eden和Survivor區之間發生大量的copy,新生代GC採用複製算法)。

  • 3.長期存活的對象進入老年代

長期存活的對象將進入老年代,若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,並將對象年齡設爲1。對象在Survivor區中每熬過一次MinorGC,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲)時,就會被晉升到老年代中。-XX:MaxTenuringThreshold 用來定義年齡的閾值

  • 4.動態對象年齡斷定

虛擬機並非永遠地要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡≥該年齡的對象就能夠直接進入老年代。

  • 5.空間分配擔保

在發生 Minor GC 以前,虛擬機先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是條件成立的話,那麼 Minor GC 能夠確認是安全的。若是不成立的話虛擬機會查看 HandlePromotionFailure 設置值是否容許擔保失敗,若是容許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次 Minor GC;若是小於,或者 HandlePromotionFailure 設置不容許冒險,那麼就要進行一次 Full GC。

GC觸發條件

Minor GC:其觸發條件很是簡單,當 Eden 空間滿時,就將觸發一次 Minor GC。

Full GC:相對複雜,有如下條件:

  • 1.調用 System.gc()

只是建議虛擬機執行 Full GC,可是虛擬機不必定真正去執行。不建議使用這種方式,而是讓虛擬機管理內存。

  • 2.老年代空間不足

老年代空間不足的常見場景爲前文所講的大對象直接進入老年代、長期存活的對象進入老年代等。

爲了不以上緣由引發的 Full GC,應當儘可能不要建立過大的對象以及數組。除此以外,能夠經過 -Xmn 虛擬機參數調大新生代的大小,讓對象儘可能在新生代被回收掉,不進入老年代。還能夠經過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。

  • 3.空間分配擔保失敗

使用複製算法的 Minor GC 須要老年代的內存空間做擔保,若是擔保失敗會執行一次 Full GC。

  • 4.JDK 1.7 及之前的永久代空間不足

在 JDK 1.7 及之前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的爲一些 Class 的信息、常量、靜態變量等數據。

當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被佔滿,在未配置爲採用 CMS GC 的狀況下也會執行 Full GC。若是通過 Full GC 仍然回收不了,那麼虛擬機會拋出 java.lang.OutOfMemoryError。

爲避免以上緣由引發的 Full GC,可採用的方法爲增大永久代空間或轉爲使用 CMS GC。

  • 5.Concurrent Mode Failure

執行 CMS GC 的過程當中同時有對象要放入老年代,而此時老年代空間不足(多是 GC 過程當中浮動垃圾過多致使暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,並觸發 Full GC。

相關文章
相關標籤/搜索