深刻理解Java虛擬機(JVM) --- 垃圾收集算法(中)

2 回收無效對象的過程

當經可達性算法篩選出失效的對象以後,並非當即清除,而是再給對象一次重生的機會java

  • 判斷是否覆蓋finalize()算法

    • 未覆蓋該或已調用過該方法,直接釋放對象內存
    • 已覆蓋該方法且還未被執行,則將finalize()扔到F-Queue隊列中
  • 執行F-Queue中的finalize()
    虛擬機會以較低的優先級執行這些finalize(),不會確保全部的finalize()都會執行結束

若是finalize()中出現耗時操做,虛擬機就直接中止執行,將該對象清除segmentfault

  • 對象重生或死亡安全

    • 若是在執行finalize()方法時,將this賦給了某一個引用,則該對象重生
    • 若是沒有,那麼就會被垃圾收集器清除
注意:強烈不建議使用finalize()進行任何操做!
若是須要釋放資源,請用try-finally或者其餘方式都能作得更好.
由於finalize()不肯定性大,開銷大,沒法保證各個對象的調用順序.

如下代碼示例看到:一個對象的finalize被執行,但依然能夠存活ide

/**
 * 演示兩點:
 * 1.對象能夠在被GC時自救
 * 2.這種自救機會只有一次,由於一個對象的finalize()最多隻能被系統自動調用一次,所以第二次自救失敗
 * @author sss
 * @since 17-9-17 下午12:02
 *
 */
public class FinalizeEscapeGC {

    private static FinalizeEscapeGC SAVE_HOOK = null;

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

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


    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        // 對象第一次成功自救
        SAVE_HOOK = null;
        System.gc();
        // 由於finalize方法優先級很低,因此暫停0.5s以等待它
        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 :(");
        }
    }
}

運行結果oop

finalize methodd executed!
yes,I am still alive :)
no,I am dead :(

3 方法區的內存回收

使用複製算法實現堆的內存回收,堆被分爲新生代老年代性能

  • 新生代中的對象"朝生夕死",每次垃圾回收都會清除掉大量對象
  • 老年代中的對象生命較長,每次垃圾回收只有少許的對象被清除

因爲方法區中存放生命週期較長的類信息、常量、靜態變量.
所以方法區就像堆的老年代,每次GC只有少許垃圾被清除.this

方法區中主要清除兩種垃圾spa

  • 廢棄常量
  • 無用類

3.1 回收廢棄常量

回收廢棄常量和回收對象相似,只要常量池中的常量不被任何變量或對象引用,那麼這些常量就會被清除.線程

3.2 回收無用類

斷定無用類的條件則較爲苛刻

  • 該類全部實例都已被回收

即Java堆不存在該類的任何實例

  • 加載該類的ClassLoader已被回收
  • 該類的java.lang.Class對象沒有被任何對象或變量引用,沒法經過反射訪問該類的方法

只要一個類被虛擬機加載進方法區,那麼在堆中就會有一個表明該類的對象:java.lang.Class.這個對象在類被加載進方法區的時候建立,在方法區中該類被刪除時清除.

4 垃圾收集算法

4.1 清除(Sweep)

最基礎的收集算法,後續算法也都是基於此並改進其不足而得.

該算法會從每一個GC Roots出發,依次標記有引用關係的對象,最後將沒有被標記的對象清除

把死亡對象所佔據的內存標記爲空閒內存,並記錄在一個空閒列表(free list)之中
當須要新建對象時,內存管理模塊便會從該空閒列表中尋找空閒內存,並劃分給新建的對象。

不足

清除這種回收方式的原理及其簡單,可是有兩個缺點

內存碎片

因爲Java虛擬機的堆中對象必須是連續分佈的,所以可能出現總空閒內存足夠,可是沒法分配的極端狀況。

分配效率較低

若是是一塊連續的內存空間,那麼咱們能夠經過指針加法(pointer bumping)來作分配
而對於空閒列表,Java虛擬機則須要逐個訪問列表中的項,來查找可以放入新建對象的空閒內存。

第二種是壓縮(compact),即把存活的對象彙集到內存區域的起始位置,從而留下一段連續的內存空間。這種作法可以解決內存碎片化的問題,但代價是壓縮算法的性能開銷。
這種算法會帶來大量的空間碎片,致使須要分配一個較大連續空間時容易觸發FullGC,下降了空間利用率.

爲了解決這個問題,又提出了「標記-整理算法」,該算法相似計算機的磁盤整理,首先會從GC Roots出發標記存活的對象,而後將存活對象整理到內存空間的一端,造成連續的已使用空間,最後把已使用空間以外的部分所有清理掉,這樣就不會產生空間碎片的問題

4.2 複製算法(Copy)

把內存區域分爲兩等分,分別用兩個指針from和to來維護,而且只是用from指針指向的內存區域來分配內存。
當發生垃圾回收時,便把存活的對象複製到to指針指向的內存區域中,而且交換from指針和to指針的內容。複製這種回收方式一樣可以解決內存碎片化的問題,可是它的缺點也極其明顯,即堆空間的使用效率極其低下。

將內存分紅大小相等兩份,只將數據存儲在其中一塊上

  • 當須要回收時,首先標記廢棄數據
  • 而後將有用數據複製到另外一塊內存
  • 最後將第一塊內存空間所有清除

4.2.1 分析

  • 這種算法避免了空間碎片,但內存縮小了一半.
  • 每次都需將有用數據所有複製到另外一片內存,效率不高

4.2.2 解決空間利用率問題

堆內存空間分爲較大的Eden和兩塊較小的Survivor,每次只使用Eden和Survivor區的一塊。這種情形下的「 Mark-Copy"減小了內存空間的浪費。「Mark-Copy」現做爲主流的YGC算法進行新生代的垃圾回收。
在新生代中,因爲大量對象都是"朝生夕死",也就是一次垃圾收集後只有少許對象存活
所以咱們能夠將內存劃分紅三塊

    • Eden、Survior一、Survior2
    • 內存大小分別是8:1:1

    分配內存時,只使用Eden和一塊Survior1.

    • 當發現Eden+Survior1的內存即將滿時,JVM會發起一次Minor GC,清除掉廢棄的對象,
    • 並將全部存活下來的對象複製到另外一塊Survior2中.
    • 接下來就使用Survior2+Eden進行內存分配

    經過這種方式,只須要浪費10%的內存空間便可實現帶有壓縮功能的垃圾收集方法,避免了內存碎片的問題.

    4.2.3 分配擔保

    準備爲一個對象分配內存時,發現此時Eden+Survior中空閒的區域沒法裝下該對象
    就會觸發MinorGC(新生代 GC 算法),對該區域的廢棄對象進行回收.

    但若是MinorGC事後只有少許對象被回收,仍然沒法裝下新對象

    • 那麼此時須要將Eden+Survior中的全部對象轉移到老年代中,而後再將新對象存入Eden區.這個過程就是"分配擔保".

    在發生 minor gc 前,虛擬機會檢測老年代最大可用連續空間是否大於新生代全部對象總空間
    若成立,minor gc 可確保安全
    若不成立,JVM會查看 HandlePromotionFailure是否容許擔保失敗

    • 若容許

    那麼會繼續檢測老年代最大可用的連續空間是否 > 歷次晉升到老年代對象的平均大小

    • 若大於

    則將嘗試進行一次 minor gc,儘管此次 minor gc 是有風險的

    • 若小於或 HandlePromotionFailure 設置不容許冒險

    改成進行一次 full gc (老年代GC)

    4.3 壓縮算法(Compact)

    在回收前,標記過程仍與"清除"同樣
    但後續不是直接清理可回收對象,而是

    • 將全部存活對象移到一端
    • 直接清掉端邊界以外內存

    分析

    這是一種老年代垃圾收集算法.
    老年代中對象通常壽命較長,每次垃圾回收會有大量對象存活
    所以若是選用"複製"算法,每次須要較多的複製操做,效率低

    並且,在新生代中使用"複製"算法
    當 Eden+Survior 都裝不下某個對象時,可以使用老年代內存進行"分配擔保"

    而若是在老年代使用該算法,那麼在老年代中若是出現 Eden+Survior 裝不下某個對象時,沒有其餘區域給他做分配擔保

    所以,老年代中通常使用"壓縮"算法

    4.4 分代收集算法(Generational Collection)

    當前商業虛擬機都採用此算法.
    根據對象存活週期的不一樣將Java堆劃分爲老年代和新生代,根據各個年代的特色使用最佳的收集算法.

    • 老年代中對象存活率高,無額外空間對其分配擔保,必須使用"標記-清除"或"標記-壓縮"算法
    • 新生代中存放"朝生夕死"的對象,用複製算法,只須要付出少許存活對象的複製成本,就可完成收集

    5 Java中引用的種類

    Java中根據生命週期的長短,將引用分爲4類

    • 強引用

    咱們平時所使用的引用就是強引用
    相似A a = new A();
    即經過關鍵字new建立的對象所關聯的引用就是強引用
    只要強引用還存在,該對象永遠不會被回收

    • 軟引用

    一些還有用但並不是必需的對象
    只有當堆即將發生OOM異常時,JVM纔會回收軟引用所指向的對象.
    軟引用經過SoftReference類實現
    軟引用的生命週期比強引用短一些

    • 弱引用

    也是描述非必需對象,比軟引用更弱
    所關聯的對象只能存活到下一次GC發生前.
    只要垃圾收集器工做,不管內存是否足夠,弱引用所關聯的對象都會被回收.
    弱引用經過WeakReference類實現.

    • 虛引用

    也叫幽靈(幻影)引用,最弱的引用關係.
    它和沒有引用沒有區別,沒法經過虛引用取得對象實例.
    設置虛引用惟一的做用就是在該對象被回收以前收到一條系統通知.
    虛引用經過PhantomReference類來實現.

    總結

    Java虛擬機中的垃圾回收器採用可達性分析來探索全部存活的對象。它從一系列GC Roots出發,邊標記邊探索全部被引用的對象。

    爲了防止在標記過程當中堆棧的狀態發生改變,Java虛擬機採起安全點機制來實現Stop-the-world操做,暫停其餘非垃圾回收線程。

    回收死亡對象的內存共有三種方式,分別爲:會形成內存碎片的清除、性能開銷較大的壓縮、以及堆使用效率較低的複製。

    今天的實踐環節,你能夠體驗一下無安全點檢測的計數循環帶來的長暫停。你能夠分別測單獨跑foo方法或者bar方法的時間,而後與合起來跑的時間比較一下。

    // time java SafepointTestp
    // 還可使用以下幾個選項
    // -XX:+PrintGC
    // -XX:+PrintGCApplicationStoppedTime 
    // -XX:+PrintSafepointStatistics
    // -XX:+UseCountedLoopSafepoints
    public class SafepointTest {
      static double sum = 0;
    
      public static void foo() {
        for (int i = 0; i < 0x77777777; i++) {
          sum += Math.sqrt(i);
        }
      }
    
      public static void bar() {
        for (int i = 0; i < 50_000_000; i++) {
          new Object().hashCode();
        }
      }
    
      public static void main(String[] args) {
        new Thread(SafepointTest::foo).start();
        new Thread(SafepointTest::bar).start();
      }
    }

    參考

    java的gc爲何要分代?
    深刻理解Java虛擬機(第2版)
    深刻拆解Java虛擬機

    本文由博客一文多發平臺 OpenWrite 發佈!
    相關文章
    相關標籤/搜索