JVM垃圾回收-finalization機制& 安全點& 安全區域(二)

2020-09-28 13:00:49

finalization機制概述

  • 當垃圾回收時物件被銷燬前, 總會先呼叫物件的 finalize()方法(被呼叫的前提是必須重寫此方法, 同時未被呼叫過, 因為此方法只會呼叫一次), 常用於處理資源釋放 如關閉檔案, 通訊端, 資料庫連線等

物件的3種狀態

  • 當一個物件與 GC Roots失去聯絡時, 就意味著物件已經不再使用了. 通常進入垃圾回收時就會被回收, 但是如果重寫了 finalize()方法, 虛擬機器器不會立馬回收, 而呼叫 finalize()方法緩一次回收(也就是給了一次復活的機會)
  1. 可觸及的: 可達物件(與 GC Roots直接或間接的聯絡著)
  2. 可復活的: 不可達物件(失去了與 GC Roots的聯絡), 但有一次執行 finalize()方法緩一次回收的機會(此時可以編寫復活相關邏輯)
  3. 不可觸及的: 已呼叫過一次 finalize()方法, 且沒有復活. 此時的狀態就是不可觸及的狀態. 此時進入回收, 將肯定會被回收

銷燬物件之前具體判斷過程

  • 判定一個物件A是否可回收, 會經過兩次標記過程:
  1. 如果物件A與 GC Roots失去聯絡, 則進行第一次標記
  2. 是否執行 finalize()方法的判斷過程:
    (1) 物件A重寫了 finalize()方法(如沒有重寫意味著沒有復活的過程), 同時 finalize()方法已呼叫過一次, 則判定物件A為不可觸及的狀態
    (2) 物件A重寫了 finalize()方法, 且還未執行過, 那麼物件A會被插入到一種 F-Queue佇列(參照佇列)中, 由一個虛擬機器器自動建立的, 低優先順序的 finalizer執行緒觸發其 finalize()方法執行
    (3) - 稍後(finalizer執行緒的觸發後), 將會對 F-Queue佇列中的物件進行第二次標記. 此時如果物件A在 finalize()中復活了(也就是重新建立了, 與 GC Roots的聯絡), 那麼在此次標記時, 物件A會從’即將回收’的集合中移出. 之後, 如果物件A再次出現與 GC Roots失去聯絡的情況, 會直接成為不可觸及的狀態, 因為 finalize()方法只會被呼叫一次

演範例子


public class FinalizeTestApp {
    /** GC Roots*/
    public static FinalizeTestApp objA;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("invoking `finalize` by GC");
        /** 賦予參照地址, 目的為復活*/
        objA = this;
    }

    public static void main(String[] args) {
        try {
            objA = new FinalizeTestApp();
            /** 刪除參照地址*/
            objA = null;
            /** System.gc()或 Runtime.getRuntime().gc(); 會顯式的觸發 Full GC
             * 即使直接呼叫 以上方法, 也無法保證對垃圾收集器的呼叫
             * */
            System.gc();
            System.out.println("GC 1");
            /** 由於 Finalizer執行緒優先順序較低, 暫停2秒, 為了保證 GC的執行*/
            Thread.sleep(2000);
            if (objA == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }

            /** 重複了上面程式碼, 為了演示 finalize()的被呼叫次數*/
            objA = null;
            System.gc();
            System.out.println("GC 2");
            /** 由於 Finalizer執行緒優先順序較低, 暫停2秒, 為了保證 GC的執行*/
            Thread.sleep(2000);
            if (objA == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

輸出:
> GC 1
> invoking `finalize` by GC
> obj is still alive
> GC 2
> obj is dead

  • 注: 永遠不要主動呼叫物件的 finalize()方法, 應由垃圾回收機制自動呼叫, 理由3點:
  1. 在 finalize()時可能會導致物件復活
  2. finalize()方法的執行時間是沒有保障的, 它完全由 GC執行緒決定, 極端情況下, 若不發生GC, 則 finalize()方法將沒有執行機會
  3. 一個糟糕的 finalize()會嚴重影響 GC的效能(重寫的情況)
  • finalize()方法區別於 C++的解構函式, 雖然比較相似, 但 Java是基於垃圾收集器的自動記憶體管理機制, 所以本質上不同於 C++的解構函式

安全點與安全區域

安全點(Safepoint)

  • 程式執行時並非在所有地方都能停頓下來開始 GC, 只有在特定的位置才能停頓下來進行 GC, 這些點成為安全點
  • 當 JVM要觸發 GC, 偏向鎖解除等操作時, 所有的使用者執行緒都必須到達安全點

安全區域(Safe Region)

  • 安全點保證了程式執行過程中的時間隔比較近的安全點. 但是有些線是處於睡眠/阻塞/等待執行的狀態(此時執行緒是無法響應 JVM的中斷請求的), 此時就會用到安全區域
  • 安全區域是指在一段程式碼片段中, 物件的參照關係不會發生變化, 在這個區域中的任何位置開始 GC都是安全的
  • 當執行緒執行到安全區域的程式碼片段時, 首先會標識已經進入了安全區域, 如果這段時間內發生 GC, JVM會忽略標識為安全區域的執行緒
  • 當執行緒即將離開安全區域時, JVM會檢查是否已經完成 GC, 如果完成了, 則繼續執行, 否則執行緒必須等待直到收到可以安全離開安全區域的訊號為止

如果您覺得有幫助,歡迎點贊哦 ~ 謝謝!!