天天看點

深入了解Java虛拟機(JVM) --- 垃圾收集算法(中)2 回收無效對象的過程3 方法區的記憶體回收4 垃圾收集算法5 Java中引用的種類總結參考

2 回收無效對象的過程

當經可達性算法篩選出失效的對象之後,并不是立即清除,而是再給對象一次重生的機會

  • 判斷是否覆寫finalize()
    • 未覆寫該或已調用過該方法,直接釋放對象記憶體
    • 已覆寫該方法且還未被執行,則将finalize()扔到F-Queue隊列中
  • 執行F-Queue中的finalize()

    虛拟機會以較低的優先級執行這些finalize(),不會確定所有的finalize()都會執行結束

如果finalize()中出現耗時操作,虛拟機就直接停止執行,将該對象清除

  • 對象重生或死亡
    • 如果在執行finalize()方法時,将this賦給了某一個引用,則該對象重生
    • 如果沒有,那麼就會被垃圾收集器清除

注意:強烈不建議使用finalize()進行任何操作!

如果需要釋放資源,請用try-finally或者其他方式都能做得更好.

因為finalize()不确定性大,開銷大,無法保證各個對象的調用順序.

以下代碼示例看到:一個對象的finalize被執行,但依然可以存活

/**
 * 示範兩點:
 * 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 :(");
        }
    }
}           

運作結果

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

3 方法區的記憶體回收

使用

複制算法

實作堆的記憶體回收,堆被分為

新生代

老年代

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

由于方法區中存放生命周期較長的類資訊、常量、靜态變量.

是以方法區就像堆的老年代,每次GC隻有少量垃圾被清除.

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

  • 廢棄常量
  • 無用類

3.1 回收廢棄常量

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

3.2 回收無用類

判定無用類的條件則較為苛刻

  • 該類所有執行個體都已被回收

    即Java堆不存在該類的任何執行個體

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

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

4 垃圾收集算法

4.1 清除(Sweep)

最基礎

的收集算法,後續算法也都是基于此并改進其不足而得.

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

把死亡對象所占據的記憶體标記為空閑記憶體,并記錄在一個空閑清單(free list)之中

當需要建立對象時,記憶體管理子產品便會從該空閑清單中尋找空閑記憶體,并劃分給建立的對象。

深入了解Java虛拟機(JVM) --- 垃圾收集算法(中)2 回收無效對象的過程3 方法區的記憶體回收4 垃圾收集算法5 Java中引用的種類總結參考

不足

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

記憶體碎片

由于Java虛拟機的堆中對象必須是連續分布的,是以可能出現總空閑記憶體足夠,但是無法配置設定的極端情況。

配置設定效率較低

如果是一塊連續的記憶體空間,那麼我們可以通過指針加法(pointer bumping)來做配置設定

而對于空閑清單,Java虛拟機則需要逐個通路清單中的項,來查找能夠放入建立對象的空閑記憶體。

第二種是壓縮(compact),即把存活的對象聚集到記憶體區域的起始位置,進而留下一段連續的記憶體空間。這種做法能夠解決記憶體碎片化的問題,但代價是壓縮算法的性能開銷。

這種算法會帶來大量的空間碎片,導緻需要配置設定一個較大連續空間時容易觸發FullGC,降低了空間使用率.

為了解決這個問題,又提出了“标記-整理算法”,該算法類似計算機的磁盤整理,首先會從GC Roots出發标記存活的對象,然後将存活對象整理到記憶體空間的一端,形成連續的已使用空間,最後把已使用空間之外的部分全部清理掉,這樣就不會産生空間碎片的問題

4.2 複制算法(Copy)

把記憶體區域分為兩等分,分别用兩個指針from和to來維護,并且隻是用from指針指向的記憶體區域來配置設定記憶體。

當發生垃圾回收時,便把存活的對象複制到to指針指向的記憶體區域中,并且交換from指針和to指針的内容。複制這種回收方式同樣能夠解決記憶體碎片化的問題,但是它的缺點也極其明顯,即堆空間的使用效率極其低下。

将記憶體分成大小相等兩份,隻将資料存儲在其中一塊上

  • 當需要回收時,首先标記廢棄資料
  • 然後将有用資料複制到另一塊記憶體
  • 最後将第一塊記憶體空間全部清除
深入了解Java虛拟機(JVM) --- 垃圾收集算法(中)2 回收無效對象的過程3 方法區的記憶體回收4 垃圾收集算法5 Java中引用的種類總結參考

4.2.1 分析

  • 這種算法避免了空間碎片,但記憶體縮小了一半.
  • 每次都需将有用資料全部複制到另一片記憶體,效率不高

4.2.2 解決空間使用率問題

堆記憶體空間分為較大的Eden和兩塊較小的Survivor,每次隻使用Eden和Survivor區的一塊。這種情形下的“ Mark-Copy"減少了記憶體空間的浪費。“Mark-Copy”現作為主流的YGC算法進行新生代的垃圾回收。

在新生代中,由于大量對象都是"朝生夕死",也就是一次垃圾收集後隻有少量對象存活

是以我們可以将記憶體劃分成三塊

  • Eden、Survior1、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)

在回收前,标記過程仍與"清除"一樣

但後續不是直接清理可回收對象,而是

  • 将所有存活對象移到一端
  • 直接清掉端邊界之外記憶體
    深入了解Java虛拟機(JVM) --- 垃圾收集算法(中)2 回收無效對象的過程3 方法區的記憶體回收4 垃圾收集算法5 Java中引用的種類總結參考

分析

這是一種老年代垃圾收集算法.

老年代中對象一般壽命較長,每次垃圾回收會有大量對象存活

是以如果選用"複制"算法,每次需要較多的複制操作,效率低

而且,在新生代中使用"複制"算法

當 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();
  }
}           

參考