深入淺出JVM(十一)之如何判斷物件“已死”

發表於2024-03-03

在方法中會建立大量的物件,物件並不一定是全域性都會使用的,並且Java虛擬機器器的資源是有限的

當JVM(Java虛擬機器器)判斷物件不再使用時,就會將其回收,避免佔用資源

那麼JVM是如何判斷物件不再使用的呢?

本篇文章將圍繞判斷物件是否再使用,深入淺出的解析引用計數法、可達性分析演演算法以及JVM如何判斷物件是真正的“死亡”(不再使用)

判斷物件已死

引用計數演演算法

引用計數演演算法判斷物件已死

在物件新增一個引用計數器,有地方引用此物件該引用計數器+1,引用失效時該引用計數器-1;當引用計數器為0時,說明沒有任何地方引用物件,物件可以被回收

image-20201119213937671.png

但是該方法無法解決迴圈引用(比如物件A的欄位引用了物件B,物件B的欄位引用了欄位A,此時都將null賦值給物件A,B它們的引用計數器上都不為0,也就是表示物件還在被引用,但實際上已經沒有引用了)

  • 優點 : 標記“垃圾”物件簡單,高效
  • 缺點: 無法解決迴圈引用,儲存引用計數器的空間開銷,更新引用記數的時間開銷

因為無法解決迴圈引用所以JVM不使用引用計數法

引用計數方法常用在不存在迴圈引用的時候,比如Redis中使用引用計數,不存在迴圈引用

證明Java未採用引用計數演演算法
 public class ReferenceCountTest {
     //佔用記憶體
     private static final byte[] MEMORY = new byte[1024 * 1024 * 2];
 ​
     private ReferenceCountTest reference;
 ​
     public static void main(String[] args) {
         ReferenceCountTest a = new ReferenceCountTest();
         ReferenceCountTest b = new ReferenceCountTest();
         //迴圈引用
         a.reference = b;
         b.reference = a;
 ​
         a = null;
         b = null;
 //        System.gc();
     }
 }

image-20210501210039329.png

可達性分析演演算法

Java使用可達性分析演演算法,可以解決迴圈引用

可達性分析演演算法判斷物件已死
  • GC Roots物件開始,根據引用關係向下搜尋,搜尋的過程叫做引用鏈

    • 如果透過GC Roots可以透過引用鏈達到某個物件則該物件稱為引用可達物件
    • 如果透過GC Roots到某個物件沒有任何引用鏈可以達到,就把此物件稱為引用不可達物件,將它放入引用不可達物件集合中(如果它是首個引用不可達物件節點,那它就是引用不可達物件根節點)

image-20221222092632373.png

可以作為GC Roots物件的物件
  1. 在棧幀中區域性變數表中引用的物件引數、臨時變數、區域性變數
  2. 本地方法引用的物件
  3. 方法區的類變數引用的物件
  4. 方法區的常量引用的物件(字串常量池中的引用)
  5. sychronized同步鎖持有的物件
  6. JVM內部引用(基礎資料型別對應的Class物件、系統類載入器、常駐異常物件等)
  7. 跨代引用
  • 缺點:

    • 使用可達性分析演演算法必須在保持一致性的快照中進行(某時刻靜止狀態)
    • 這樣在進行GC時會導致STW(Stop the Word)從而讓使用者執行緒短暫停頓

真正的死亡

真正的死亡最少要經過2次標記

  • 透過GC Roots經過可達性分析演演算法,得到某物件不可達時,進行第一次標記該物件
  • 接著進行一次篩選(篩選條件: 此物件是否有必要執行finalize()

    • 如果此物件沒有重寫finalize()或JVM已經執行過此物件的finalize()都將被認為此物件沒有必要執行finalize(),這個物件真正的死亡了
    • 如果認為此物件有必要執行finalize()則會把該物件放入F-Queue佇列中,JVM自動生成一條低優先順序的Finalizer執行緒

      • Finalizer執行緒是守護執行緒,不需要等到該執行緒執行完才結束程式,也就是說不一定會執行該物件的finalize()方法
      • 設計成守護執行緒也是為了防止執行finalize()時會發生阻塞,導致程式時間很長,等待很久
      • Finalize執行緒會掃描F-Queue佇列,如果此物件的finalize()方法中讓此物件重新與引用鏈上任一物件搭上關係,那該物件就完成自救finalize()方法是物件自救的最後機會
測試不重寫finalize()方法,物件是否會自救
 /**
  * @author Tc.l
  * @Date 2020/11/20
  * @Description:
  * 測試不重寫finalize方法是否會自救
  */
 public class DeadTest01 {
     public  static DeadTest01 VALUE = null;
     public static void isAlive(){
         if(VALUE!=null){
             System.out.println("Alive in now!");
         }else{
             System.out.println("Dead in now!");
         }
     }
     public static void main(String[] args) {
         VALUE = new DeadTest01();
 ​
         VALUE=null;
         System.gc();
         try {
             //等Finalizer執行緒執行
             TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         isAlive();
     }
 }
 /*
 Dead in now!
 */

物件並沒有發生自救,物件不再使用“已死”

測試重寫finalize()方法,物件是否會自救
 /**
  * @author Tc.l
  * @Date 2020/11/20
  * @Description:
  * 測試重寫finalize方法是否會自救
  */
 public class DeadTest02 {
     public  static DeadTest02 VALUE = null;
     public static void isAlive(){
         if(VALUE!=null){
             System.out.println("Alive in now!");
         }else{
             System.out.println("Dead in now!");
         }
     }
 ​
     @Override
     protected void finalize() throws Throwable {
         super.finalize();
         System.out.println("搭上引用鏈的任一物件進行自救");
         VALUE=this;
     }
 ​
     public static void main(String[] args) {
         VALUE = new DeadTest02();
         System.out.println("開始第一次自救");
         VALUE=null;
         System.gc();
         try {
             //等Finalizer執行緒執行
             TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         isAlive();
 ​
         System.out.println("開始第二次自救");
         VALUE=null;
         System.gc();
         try {
             //等Finalizer執行緒執行
             TimeUnit.SECONDS.sleep(1);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         isAlive();
     }
 }
 /*
 開始第一次自救
 搭上引用鏈的任一物件進行自救
 Alive in now!
 開始第二次自救
 Dead in now!
 */

第一次自救成功,第二次自救失敗,說明瞭finalize()執行過,JVM會認為它是沒必要執行的了

重寫finalize()代價高,不能確定各個物件執行順序,不推薦使用

總結

本篇文章圍繞如何判斷物件不再使用,深入淺出的解析引用計數法、可達性分析演演算法以及JVM中如何真正確定物件不再使用的

引用計數法使用計數器來記錄物件被引用的次數,當發生迴圈引用時無法判斷物件是否不再使用,因此JVM沒有使用引用計數法

可達性分析演演算法使用從根節點開始遍歷根節點的引用鏈,如果某個物件在引用鏈上說明這個物件被引用是可達的,不可達物件則額外記錄

可達性分析演演算法需要在保持一致性的快照中進行,在GC時會發生STW短暫的停頓使用者執行緒

可達性分析演演算法中的根節點一般是區域性變數表中引用的物件、方法中引用的物件、方法區靜態變數引用的物件、方法區常量引用的物件、鎖物件、JVM內部引用物件等等

當物件不可達時,會被放在佇列中由finalize守護執行緒來依次執行佇列中物件的finalize方法,如果第一次在finalize方法中搭上引用鏈則又會變成可達物件,注意finalize方法只會被執行一次,後續再不可達則會被直接認為物件不再使用

最後(一鍵三連求求拉~)

本篇文章筆記以及案例被收入 gitee-StudyJavagithub-StudyJava 感興趣的同學可以stat下持續關注喔\~

有什麼問題可以在評論區交流,如果覺得菜菜寫的不錯,可以點贊、關注、收藏支援一下\~

關注菜菜,分享更多幹貨,公眾號:菜菜的後端私房菜

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章