剖析垃圾回收機制(上)

帥氣的碼農發表於2020-07-28
前言:
關於 JVM 垃圾回收機制面試中主要涉及這三個考題:
  •  JVM 中有哪些垃圾回收演算法?它們各自有什麼優劣?
  • CMS 垃圾回收器是怎麼工作的?有哪些階段?
  • 服務卡頓的元凶到底是誰?
雖然 Java 不用“手動管理”記憶體回收,程式碼寫起來很順暢。但是你有沒有想過,這些記憶體是怎麼被回收的?其實,JVM 是有專門的執行緒在做這件事情。當我們的記憶體空間達到一定條件時,會自動觸發。這個過程就叫作 GC,負責 GC 的元件,就叫作垃圾回收器。VM 規範並沒有規定垃圾回收器怎麼實現,它只需要保證不要把正在使用的物件給回收掉就可以。在現在的伺服器環境中,經常被使用的垃圾回收器有 CMS 和 G1,但 JVM 還有其他幾個常見的垃圾回收器。按照語義上的意思,垃圾回收。
 
首先就需要找到這些垃圾,然後回收掉。但是 GC 過程正好相反,它是先找到活躍的物件,然後把其他不活躍的物件判定為垃圾,然後刪除。所以垃圾回收只與活躍的物件有關,和堆的大小無關。這個概念是我們一直在強調的,你一定要牢記。首先介紹幾種非常重要的回收演算法,然後著重介紹分代垃圾回收的記憶體劃分和 GC 過程,最後介紹當前 JVM 中的幾種常見垃圾回收器。
為什麼這部分這麼重要呢?是因為幾乎所有的垃圾回收器,都是在這些基本思想上演化出來的!!
 
1.垃圾回收的第一步,就是找出活躍的物件。我們反覆強調 GC 過程是逆向的。根據 GC Roots 遍歷所有的可達物件,這個過程,就叫作標記。
 
如圖所示,圓圈代表的是物件。綠色的代表 GC Roots,紅色的代表可以追溯到的物件。可以看到標記之後,仍然有多個灰色的圓圈,它們都是被回收的物件。
 
清除(Sweep)
清除階段就是把未被標記的物件回收掉。
但是這種簡單的清除方式,有一個明顯的弊端,那就是碎片問題。比如我申請了 1k、2k、3k、4k、5k 的記憶體。
由於某種原因 ,2k 和 4k 的記憶體,我不再使用,就需要交給垃圾回收器回收。
這個時候,我應該有足足 6k 的空閒空間。接下來,我打算申請另外一個 5k 的空間,結果系統告訴我記憶體不足了。系統執行時間越長,這種碎片就越多。
在很久之前使用 Windows 系統時,有一個非常有用的功能,就是記憶體整理和磁碟整理,執行之後有可能會顯著提高系統效能。這個出發點是一樣的。
 
複製(Copy)
 
解決碎片問題沒有銀彈,只有老老實實的進行記憶體整理。
有一個比較好的思路可以完成這個整理過程,就是提供一個對等的記憶體空間,將存活的物件複製過去,然後清除原記憶體空間。在程式設計中,一般遇到擴縮容或者碎片整理問題時,複製演算法都是非常有效的。比如:HashMap 的擴容也是使用同樣的思路,Redis 的 rehash 也是類似的。整個過程如圖所示:
這種方式看似非常完美的,解決了碎片問題。但是,它的弊端也非常明顯。它浪費了幾乎一半的記憶體空間來做這個事情,如果資源本來就很有限,這就是一種無法容忍的浪費。
 
整理(Compact)
 
其實,不用分配一個對等的額外空間,也是可以完成記憶體的整理工作。你可以把記憶體想象成一個非常大的陣列,根據隨機的 index 刪除了一些資料。那麼對整個陣列的清理,其實是不需要另外一個陣列來進行支援的,使用程式就可以實現。它的主要思路,就是移動所有存活的物件,且按照記憶體地址順序依次排列,然後將末端記憶體地址以後的記憶體全部回收。
 
 
我們可以用一個理想的演算法來看一下這個過程。
last = 0
for(i=0;i<mems.length;i++){
  if(mems[i] != null){
      mems[last++] = mems[i]
      changeReference(mems[last])
  }
}
clear(mems,last,mems.length)
但是需要注意,這只是一個理想狀態。物件的引用關係一般都是非常複雜的,我們這裡不對具體的演算法進行描述。你只需要瞭解,從效率上來說,一般整理演算法是要低於複製演算法的。

分代

 

相關文章