Java&Android 基礎知識梳理(4) 垃圾收集器與記憶體分配策略

澤毛發表於2017-12-21

一、概述

GC需要考慮的三個問題:

  • 哪些記憶體需要回收
  • 什麼時候回收
  • 如何回收

在分析記憶體區域的時候,我們把Java執行時資料區分為兩個部分:

  • 程式計數器、虛擬機器棧、本地方法棧:每個棧幀中分配多少記憶體在類結構確定下來就已知,因此這些區域的記憶體分配和回收具備確定性,方法結束或執行緒結束時,記憶體就跟著被回收了。
  • Java堆、方法區:由於一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也不一樣,只有在程式處於執行期間才能知道會建立哪些物件,因此這些區域的記憶體分配和回收是動態的。

二、如何判斷哪些是“存活”的例項

2.1 引用的分類

引用的定義:如果reference型別的資料中儲存的數值代表的另外一塊記憶體的起始地址,就稱這塊記憶體代表引用。 引用的分類:

  • 強引用(Object a = new Object()):只要強引用存在,垃圾回收器永遠不會回收掉被引用的物件。
  • 軟引用(SoftReference):有用但並非必須,在系統將要發生OOM異常之前,將會把這些物件列進回收範圍中進行第二次回收。
  • 弱引用(WeakReference):非必須物件,被弱引用的物件只能生存到下一次垃圾收集發生前。
  • 虛引用(PhantomReference):不會對生存時間產生影響,也無法通過虛引用來取得一個物件例項,設定虛引用的唯一目的就是能在這個物件被垃圾回收器回收時收到一個系統通知。

2.2 引用計數法

給物件新增一個引用計數器,當有一個地方引用它時就加一,引用失效時就減一,當計數器的值為零時表示它不可用。 但是它無法解決相互迴圈引用問題

2.3 可達性分析

通過一系列的稱為GC Roots的物件作為起始點,從這些節點開始向下搜尋,所走過的路徑稱為引用鏈。當一個物件到GC Roots沒有任何引用鏈時,表示這個物件不可用,GC Roots的型別有:

  • 虛擬機器棧中的區域性變數表中引用的物件。
  • 方法區中類靜態屬性引用的物件。
  • 方法區中常量引用的物件。
  • 本地方法棧中**JNI**引用的物件。

2.4 finalize方法對於記憶體回收的影響

當某個物件在經過可達性分析後,發現它到GC Roots沒有任何引用鏈時,那麼它會被第一次標記,並進行第一次篩選,篩選的結果有兩種情況:

  • 沒有覆蓋finalize()方法或者虛擬機器已經呼叫過它的finalize()方法:直接回收。
  • 其它情況:把這個物件放置在一個F-Queue的佇列中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行這個物件的finalize()方法,如物件要在finalize方法中拯救自己,只要重新與引用鏈的某個變數關聯即可,那麼在第二次標記時它將被移出“即將回收”的集合,否則它將被回收。

這種方法代價高昂,不確定性大,無法保證各個物件的呼叫順序,因此可以忘記這個方法的存在。

三、方法區的回收

對於方法區(HotSpot中的永久代)主要回收兩部分內容:廢棄常量和無用的類。

  • 廢棄常量 以常量池中字面量的回收為例,如果一個字串abc被放入了常量池中,但是沒有任何一個String物件引用它,那麼就會被清理出常量池,常量池中其它類(介面)、方法、欄位的符號引用也類似。
  • 類 同時滿足三個條件:
  • 該類的所有例項已經被回收
  • 載入該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過發射訪問該類的方法。

四、垃圾收集演算法基礎

4.1 標記 - 清除演算法

  • 概念 首先標記出所有需要回收的物件,在標記完成後統一進行回收。
  • 缺點:
  • 標記和清除兩個過程效率不高。
  • 產生記憶體碎片,導致需要分配較大物件時,無法找到足夠的連續記憶體而需要觸發一次GC操作。

4.2 複製演算法

  • 概念 將可用記憶體劃分為大小相等的兩塊,每次只使用其中的一塊,當一塊記憶體用完了。則觸發一次GC操作,將活著的物件複製到另一塊上,然後再把已使用的記憶體空間一次清理掉。

  • 缺點 將記憶體縮小為了原來的一半。

  • 現在商業虛擬機器採用這種演算法的改良版來實現新生代的回收 它把記憶體按8:1:1分為Eden/survivor0/survivor1三塊: 需要分配記憶體時,首先嚐試在Eden區分配,如果Eden區無法分配,那麼嘗試把活著的物件放到survivor0中去:

  • 如果survivor0可以放入,那麼放入之後清除Eden區。

  • 如果survivor0不可以放入,那麼嘗試把Edensurvivor0的存活物件放到survivor1中:

    • 如果survivor1可以放入,那麼放入survivor1之後清除Edensurvivor0,之後再把survivor1中的物件複製到survivor0中,保持survivor1一直為空。
    • 如果survivor1不可以放入,那麼直接把它們放入到老年代中,並清除Edensurvivor0,這個過程也稱為分配擔保
  • 適用情況 由於複製演算法在物件成活率較高時,需要較多的複製操作,效率會變低,所以在老年代中不能採用該演算法。

4.3 標記 - 整理演算法

  • 概念 和標記 - 清除演算法類似,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清除掉端邊界以外的記憶體。
  • 優點 解決了標記- 清除演算法導致的記憶體碎片問題在存活率較高時複製演算法效率低的問題

4.4 分代收集演算法

當前商業虛擬機器採用的方式,根據物件存活週期的不同將記憶體劃分為幾塊,一般是新生代和老年代:

  • 新生代:每次垃圾收集時只有少量存活,選用複製演算法的改良版,也就是上面說到的Eden/survivor0/survivor1的分配方式。
  • 老年代:物件存活率較高,且沒有分配擔保,必須用標記 - 清除或標記 - 整理演算法來實現。

五、Minor GCMajor GC/Full GC

  • Minor GC:發生在新生代的垃圾回收動作,非常頻繁,回收速度也較快,採用的垃圾收集器有SerialParNewParallel Scavenge
  • Major GC/Full GC:發生在老年代的GC,經常伴隨至少一次的Minor GCMajor GC的速度一般會比Minor GC慢十倍以上,採用的垃圾收集器有CMSSerial OldParallel Old

六、物件分配的原則

  • 物件優先在Eden區分配 當Eden區沒有足夠空間,觸發一次Minor GC

  • 大物件直接進入老年代 例如很長的字串以及陣列,經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。

  • 長期存活的物件將進入老年代 如果Eden區出生並進過第一次Minor GC後,仍然存活,並且被成功複製到survivor區中,那麼物件年齡變為一,當物件在survivor中每熬過一次Minor GC,年齡就增加一,當年齡增加到一定程度,就會晉升到老年代中。

  • 動態物件年齡繫結 如果survivor空間中相同年齡所有物件大小的總和大於survivor空間的一半,年齡大於或等於該年齡的物件就可以進入老年代,無須到達要求的年齡。

  • 空間分配擔保 在發生Minor GC前,檢查老年代最大可用連續空間是否大於新生代所有物件總空間:

  • 大於,那麼操作是安全的,不對老年代進行Full GC

  • 小於,檢查HandlePromotionFailure設定值是否允許失敗:

    • 允許:檢查老年代最大可用連續空間是否大於歷次晉升到老年代物件的平均大小:
      • 大於:不對老年代進行Full GC。在這之後,因為有可能出現某次存活物件激增的情況,這種屬於冒險行為,如果出現了擔保失敗(也就是Edensurvivor0的存活物件既無法放入survivor1,也無法放入老年代的連續空間中),那麼會在失敗之後對老年代進行Full GC
      • 小於:先對老年代進行一次Full GC
    • 不允許:先對老年代執行一次Full GC

相關文章