深入理解 Java 虛擬機:GC垃圾收集器及相關算法

判斷哪些對象需要回收

Java 堆裏存放着幾乎所有的對象實例,因此在回收前需要判斷哪些對象是 “存活” 的,這些對象不需要回收,只回收已經 “死去” 的對象(即不可能再被任何途徑使用的對象)。

引用計數器算法

算法原理:
給對象添加一個引用計數器,每當有一個地方引用它時,計算器 +1;當引用失效時,計數器 -1;任何時刻計數器爲 0 的對象就是不可能再被使用的,可以被回收。

優點: 實現簡單,判斷效率高
缺點: 無法解決對象間相互引用的問題
應用: Python 語言,遊戲腳本領域使用的 Squirrel 等

可達性分析算法

算法原理:
從一系列稱爲 “GC Roots” 的對象爲起點,沿着引用鏈向下搜索,當一個對象到 GC Roots 沒有任何引用鏈相連,則證明此對象可以被回收(即 GC Roots 無法到達此對象)。

GC Roots 的對象:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。
    對象在創建時候,會在堆上開闢一個空間用於分配實例,之後把堆的地址作爲引用存放在中,在對象生命週期結束後,引用就會從虛擬機棧中出棧

  2. 方法區(永久代)中類靜態屬性引用的對象。
    即被 static 修飾的靜態對象,存放在方法區

  3. 方法區(永久代)中常量引用的對象。
    即被 staticfinal 修飾的對象,存放在方法區

  4. 本地方法棧中 JNI(即一般說的 Native 方法)引用的對象。
    JNIJava Native Interface,主要用於幫助 Java 與 別的語言進行通信(主要 C,C++)提供接口。

缺點: GC停頓,爲了保證一致性,導致GC進行時必須停頓所有的 Java 執行線程。可以想象下,系統運行一半,突然像被人按了暫停鍵一樣突然卡住了,然後 GC 結束才繼續。

在這裏插入圖片描述

引用還有分類(瞭解)

目的: 我們希望描述這樣的一類對象,當內存空間足夠時,則保留在內存中;如果空間在回收後還非常的緊張,則可以拋棄這些對象,很多系統的緩存功能都符合這種場景。

強引用: 程序代碼中普遍存在,類似 “Object obj = new Object()” 這類的引用,只要強引用還在,垃圾收集器就永遠不可能回收這個對象

軟引用: 用來描述一些還有用但非必須的對象。這種對象在系統將要發生內存溢出錢,會把這些對象列進回收範圍進行第二次回收,依然沒有足夠內存,纔會拋出內存溢出異常。提供了 SoftReference 類來實現軟引用

弱引用: 用來描述非必須對象強度比軟引用更弱一些,被引用關聯的對象只能生存到下次垃圾收集之前。提供了 WeakReference 類來實現弱引用

虛引用: 也成爲幽靈引用或幻影引用,最弱的一張引用關係。這個引用不會對對象的生存時間產生影響,也無法通過虛引用獲取一個對象實例,這個引用的唯一目的,就是被收集器回收時收到一條系統通知。提供了 PhantomReference 類來實現虛引用

“緩刑” finalize(瞭解)

即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。

第一次標記並進行一次篩選。
篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋 finalize 方法,或者 finzlize 方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”,對象被回收。
如果對象要在 finalize() 中成功拯救自己,只要重新與引用鏈上的任何的一個對象建立關聯即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。

第二次標記,基本就是被回收了,因爲 finalize() 只會執行一次

不建議使用 finalize(),原因是運行代價高昂,不確定性大,無法保證調用順序,比如使用 try-finally 之類的方式都可以做的更好,更及時

開始垃圾收集

標記 - 清除算法

最基礎的收集算法(後面都是這個基礎的改進算法),算法分爲 “標記” 和 “清除” 兩個階段。

標記階段: Java 算法就是上面的 可達性分析算法

回收階段: 沒什麼特別的,就是直接把標記的回收了。

缺點:

  1. 效率問題,標記和清楚兩個過程效率都不高。
  2. 空間問題,由於時標記後直接就回收了,導致空間會有大量不連續的內存碎片,導致如果分配大內存對象,無法找到足夠的連續內存而不得不提前 GC。

在這裏插入圖片描述

複製算法

爲了解決效率問題,因此出現了稱爲 “複製” 的收集算法。

原理: 將內存分爲大小相等的兩塊,每次只用其中一塊。當內存用完了,把存活的對象複製到另一塊上,然後把之前已使用的空間清理掉。

優點:

  • 只對半區的內存進行回收
  • 不需要考慮內存碎片情況,因爲移到另一半空閒的上面,按順序分就行了
  • 實現簡單,運行高效

缺點: 內存變成原來的一半
在這裏插入圖片描述
改進:
現在商用虛擬機都採用這種算法回收新生代,因爲 98% 的新生代都死得快(Java 裏一個方法跑完,裏面的對象基本就都沒用了)

內存劃分:一塊較大的 Eden 空間 和 兩塊較小的 Survivor 空間,Eden 比 Survivor 大小比例是 8 比 1。

每次使用 1 塊 Eden,1 塊 Survivor,進行回收時候,會把 Eden 和 Survivor 上存活的對象都複製到另一個 Survivor 上。當 另一個 Survivor 內存不足時,需要依賴其他內存(老年代)進行分配擔保

缺點: 老年代都是存活時間較長的對象,因此這種算法不適合於老年代。

標記 - 整理算法

原理: 標記過程與 “標記 - 清除” 算法一樣,但是後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存
在這裏插入圖片描述

分代收集算法

把 Java 堆分爲新生代老年代,然後依照各自的特點採用最適當的收集算法。

規則:
新生代中如果發現有大批量的對象死去,只有少量存活,就採用複製算法

老年代中因爲對象存活率高且沒有額外的空間對它進行分配擔保,就必須使用 “標記 - 清理”“標記 - 整理” 算法來進行回收。

HotSpot 算法

雖然標記算法用的是 可達性分析算法,但是缺點存在 GC 停頓問題,因此 HotSpot 在實現方面做了修改。

枚舉根節點

即尋找所有可達性分析中的 GC Roots

爲了節省枚舉根節點而採用的解決方案:
爲了讓虛擬機知道哪裏存放着對象引用,即 GC Roots,而不是把整個執行上下文和全局引用檢查一遍,HotSpot 中使用了一組稱爲 OopMap 的數據結構來達到目的。

類加載完成的時候,把對象什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用,這樣,GC在掃描時就可以直接得知這些信息了。

安全點

在 OopMap 的協助下,HotSpot 可以快速且準確地完成 GC Roots 枚舉,但有另一個問題,OopMap 內容變化的指令非常多,如果爲每一個條指令都生成對應的OopMap,將需要大量的額外空間

解決方法:
特定的位置 記錄信息(即 OopMap 的信息),這些位置被稱爲安全點

特點:
只在遇到 安全點 才能停頓進行 GC

安全點既不能太少(內容就多了,GC 停頓就會變長),也不能過於頻繁以至於增大運行時的負荷(頻繁 GC),標準就是 “是否據有讓程序長時間執行的特徵”

因此最明顯的特徵就是指令序列複用,例如:方法調用、循環跳轉、異常跳轉等,纔會產生安全點

如何讓GC發生時,所有線程(除執行JNI調用的線程)都到最近的安全點停頓下來?

  1. 搶先式中斷,不需要線程的執行代碼主動配合,在GC發生時,首先把所有線程中斷,如果有線程中斷的地方不在安全點,就恢復線程,讓它執行到安全點

  2. 主動式中斷,需要中斷線程時,不直接對線程操作,而是設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就中斷掛起輪詢標誌的地方和安全點是重合的

安全區域

由於線程處於 SleepBlocked 的時候,線程無法響應 JVM 的中斷請求,這時候就需要 安全區域 來解決問題。

特點:
在一段代碼片段中,引用關係不會發生變化。在這個區域的任何地方 GC 都是安全的,可以把安全區域看作是安全點的擴展。

描述:
在線程執行到安全區域代碼時,首先標識自己進入安全區域,當這段時間裏 JVM 發起 GC,不用管標識爲安全區域的線程了。在線程要離開安全區域時,要檢查系統是否已經完成了根節點枚舉,如果完成,線程繼續執行,否則等待直到收到可以安全離開安全區域的信號爲止。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章