jvm的垃圾回收機制

小猴子_X發表於2022-02-16

前言:建議先了解JVM的記憶體結構才能對垃圾回收有更深的理解,可以移步JVM記憶體結構

我們都知道:java最大的特點就是實現自動記憶體管理(自動分配物件,自動垃圾回收),接下來我們就看看它是怎麼回收垃圾的。

一.垃圾回收相關演算法
垃圾回收主要有兩個階段: 標記階段 清除階段

標記階段:該階段主要為了判斷物件是否存活
  • 物件存活:有指標指向物件(物件還有可用的價值)
  • 物件銷燬:沒有指標指向物件(物件沒有可用的價值)

1.引用計數演算法

  • 對每一個物件內部儲存一個整數的引用屬性,記錄物件被引用的次數情況。 當物件被任何一個變數引用,次數就+1,當引用失效,次數-1。當次數為0,就表示該物件可以被回收
  • 優點: 實現簡單,判斷效率高,回收沒有延遲性
  • 缺點:
    • 需要給物件增加額外的空間開銷
    • 無法處理迴圈引用(致命的缺點,導致java沒有使用該演算法)

  • 但是python使用了該演算法,看看它是如何解決這個缺點
    • 手動解除引用,在合適的時候,程式設計師自己手動處理回收
    • 使用弱引用,weakref是python專門提供用來解決迴圈引用的

 2.可達性分析演算法(根搜尋演算法,追蹤性演算法)

  • 它是從GC Roots開始,從上到下根據引用判斷是否能連結到目標物件。 可以達到目標物件,就不是垃圾;達不到的物件就是垃圾。等待回收
  • 相對於引用計數演算法,執行效率就沒那麼高。但是主要可以解決迴圈引用的問題

哪些元素可以當作為"GC Roots"? (面試題)

  • 虛擬機器棧中的引用:區域性變數,方法引數等
  • 本地方法棧中的引用
  • 靜態屬性的引用:static
  • 常量的引用:static final
  • 同步監視器synchronized 持有的鎖物件
  • 臨時性加入的引用: 比如分代收集中,只針對於java堆中某一個區域進行回收。該區域的物件也有可能被別的區域的物件的屬性引用,對於該區域來說,別的區域的物件的引用也可以作為"GC Roots"。 (比如只對新生代進行回收,但是新生代的一些物件被老年代引用,那麼老年代的物件也可以作為GC Roots)     


清除階段:

 1.標記-清除(Mark-Sweep)演算法

  • 對堆記憶體從頭到尾進行線性的遍歷,發現某個物件在其Header中沒有標記為可達物件,進行回收
  • 優點: 常見,基礎。容易想到
  • 缺點: 執行效率不高 會產生記憶體碎片,需要維護一個空閒列表     
擴充套件;何為清除?

  • 不是真的置空。就是把物件的地址放在一個空閒列表中,這時物件實際還在記憶體中。 只有下次有新的物件進來佔用空間時,從空閒列表中找到空閒的地址,直接覆蓋原來的資料。

2.複製演算法

  • 背景:就是為了解決標記-清除演算法效率低的問題
  • 將堆記憶體分為兩塊,每次只使用一塊。在垃圾回收時,將存活的物件複製到未被使用的記憶體塊中,,並進行整理(放到一端)。然後將使用中記憶體塊中的所有物件都進行清除。重複此過程,完成回收
  • 優點:執行高效,保證複製過去之後空間的連續性,不會出現記憶體碎片
  • 缺點:需要兩倍的空間
  • 特別的: 如果系統的垃圾物件很多,複製演算法很理想。因為複製演算法需要複製的存活物件不多,效率就快,它適合於存活物件少,垃圾物件多的前提下。所以適用於新生代
  • 應用場景: 新生代的survivor0區和survivor1

3.標記-壓縮(Mark-Compact)演算法

  • 背景:就是對標記-清除演算法的改進,主要為了解決記憶體碎片的問題。適用於老年代
  • 將堆空間中所有物件壓縮到堆記憶體的一端,按順序排放。之後,清除邊界外所有的空間
  • 優點: (解決了其他兩個演算法的缺陷)
    • 對比標記-清除演算法,不會產生記憶體碎片
    • 對比複製演算法,消除了記憶體減半的高額代價
  • 缺點: 從效率上看,低於其他兩大演算法 (對比標記-清除演算法,還得增加整理階段)

難道就沒有一種最優的演算法嗎?
  • 無,沒有最好的演算法,只有最適合的演算法

綜合性演算法

1.分代收集演算法

  • 讓不同生命週期的物件採用不同的收集方式,以便提高回收效率
  • 比如:根據新生代和老年代的特點,分別採用不同的演算法
    • 年輕代:生命週期短,存活率低,回收頻繁。就採用複製演算法
    • 老年代:生命週期長,存活率高,回收不頻繁。就採用標記-清除或者標記-整理

2.增量收集演算法

  • 垃圾回收會產生STW,增量收集演算法就是每次讓垃圾回收只回收一小片區域的記憶體空間。那麼就讓應用執行緒和垃圾回收執行緒交替執行,儘可能減少暫停時間。
  • 缺點:因為執行緒來回切換,會使得垃圾回收的總成本上升,造成吞吐量下降

3.分割槽演算法

  • 也是為了減少暫停時間,將堆空間分割為多個小塊(region),每個region單獨回收。單獨使用。 主要是說G1回收器

 二.垃圾回收相關概念

物件的finalization機制

  • 垃圾回收器發現物件沒有引用指向的時候,就準備回收,但是回收之前,會呼叫物件的finalize()
  • 所有物件都有該方法,它允許在子類中進行重寫,一般用於物件回收之前的資源釋放。 比如:關閉檔案,套接字和資料庫連結等

注意:

  • 永遠不要主動呼叫該方法,交給垃圾回收器去呼叫
  • 該方法只能被執行一次
虛擬機器物件的三種狀態:由於finalize()方法的存在,虛擬機器物件一般處於這三種可能的狀態
  • 可觸及:物件是可達的
  • 可恢復:沒有引用指向該物件,準備被回收,但是也有可能在finalize()中被複活 (可救)
  • 不可觸及:物件的finalize()被呼叫過,沒有恢復的可能,直接回收 (不可救)
判斷物件是否可回收的過程

  • 經歷兩次標記
  1. 物件不是可達的,進行第一次標記
  2. 進行篩選,判斷物件是否有必要執行finalize()方法
    • 如果物件的finalize()已經被呼叫過,或者根本沒有重寫finalze(),沒有必要執行,直接判定為不可觸及
    • 如果物件重寫了finalize()並且沒有執行過,就會將該物件放入一個佇列中。虛擬機器自動建立的,優先順序低的Finalizer執行緒就會執行該方法。
    • 稍後GC會對上述佇列進行第二次標記,如果發現又有引用指向物件,就被移出回收集合中。如果還是沒有引用指向,直接判定為不可觸及

System.gc()的理解
  • 通過程式碼呼叫System.gc()會"顯示觸發Full GC"
  • 然而System.gc()還附帶一個免責宣告,無法保證每一次呼叫都一定會觸發Full GC。 可能就是效能測試的時候用一用

記憶體溢位與記憶體洩露
記憶體溢位:
  • 就是記憶體空間不夠,報OOM。但是隨著GC的一直髮展,一般情況下不會出現OOM,除非是應用程式佔用的記憶體增長速度非常快,垃圾回收的已經跟不上記憶體消耗的速度。
  • 造成OOM的原因:
    • java虛擬機器的堆記憶體設定不夠
    • 程式碼中建立了大量的大物件,並且長時間不能被垃圾收集器收集(存在被引用)   
  • 特別的:一般在報OOM之前就會觸發Full GC ,但是有一些情況下也可能不觸發。 比如一些超大物件.類似一個超大陣列超過堆的最大值,JVM可以判斷垃圾收集也不能解決這個問題,就直接報OOM

記憶體洩露:

  • 嚴格意義上來說,只有物件不會被引用,但是GC又不能回收他們的情況,才叫記憶體洩露,但是寬泛意義上:雖然經過可達性演算法驗證後,該物件還是被連著的。但是該物件已經不需要了,或者說沒有存在的意義了,也成為記憶體洩露
  • 記憶體洩露是可能導致OOM,不是一定會導致OOM
  • 記憶體洩露的例子:
    • 單例模式
      • 單例的生命週期和應用程式是一樣長的,所以在單例程式中,如果持有對外部物件的引用的話,,那麼這個外部物件是不能被回收的,則會導致記憶體洩露
    • 一些提供close的資源未關閉
      • 資料庫連線(connecion),網路連線(socket),io連線等。這些都需要手動關閉,否則不能回收
  • 注意:列舉迴圈引用不合適,因為迴圈引用是在引用計數演算法中才會出現。 而java是採用可達性演算法,根本不會出現迴圈引用 

圖示記憶體洩漏:


Stop The World

  •  當垃圾開始收集的時候,使用者執行緒暫停
  • 具體就是當GC進行可達性演算法分析的時候,使用者執行緒暫停
  • 任何的GC,都會發生STW。只能說盡可能縮短暫停時間

垃圾回收器中的並行與併發

  • 序列:暫停其他的執行緒(主要說其他的垃圾回收執行緒),只執行它自己的執行緒.
  • 並行:在自己的執行緒執行的過程中,其他的垃圾回收執行緒也執行 (以上都是說在垃圾執行緒進行的時候,使用者執行緒處於STW)
  • 併發:在一段時間內,自己的垃圾回收執行緒執行的過程中,使用者執行緒也在執行(範範的理解)
    • 注意:這裡的並行是:並行的是多個GC執行緒,而不是並行使用者執行緒和GC執行緒。 可以這樣理解:回收垃圾一定要讓當前使用者執行緒暫停,因為得判斷啊,就像收拾房間的時候,你也不要再製造垃圾

 


安全點與安全區域

安全點:

  • 程式執行並不是在所有的地方都可以停下來進行GC,只有在一些特定的位置才能停下來, 這些位置就稱為安全點
  • 一般安全點選擇在具有讓程式長時間執行的特徵的位置上, 比如:方法的呼叫,迴圈跳轉和異常跳轉等。

兩種方式:

  • 搶先式中斷:要發生GC了,來,所有的執行緒都停啊。看看自己是不在安全點:
    • 在,呆在原地別動。
    • 不在,你這個執行緒繼續往前跑,跑到你的安全點再停
  • 主動式中斷:設定一箇中斷標誌,所有的執行緒到達自己的安全點後,都看一下中斷標誌。(jvm採用的機制)
    • 如果中斷標誌亮了,就中斷。
    • 沒亮,繼續走。 如果一些執行緒還沒到安全點,就繼續跑,直到安全點才能判斷是否要中斷。

安全區域:

  • 剛才所有的程式都可以跑到最近的安全點。然後判斷是否安全標誌亮了。但是有一些程式, 處於休眠/阻塞狀態。雖然知道GC要來了,但是沒辦法繼續跑到下一個安全點。
  • 咋辦呢?就提出安全區域的概念,就是當程式處於不執行的時候(就是休眠/阻塞狀態),就也當做是安全的。可以進行中斷。

java的引用
面試題:強軟弱虛引用有什麼區別?具體的應用場景是什麼?
  • 強引用:不夠也不回收,我們寫的99%都是強引用。比如: String s = new String("小猴子"); 會導致記憶體溢位
  • 軟引用:記憶體不夠就回收,記憶體夠不回收。用於快取
  • 弱引用:發現就回收
  • 虛引用:追蹤物件回收資訊(主要就是當虛引用物件被回收的時候,會把虛引用放在一個引用佇列中,可以從佇列中看到物件回收的資訊)  
 Object obj =new Object();
  obj = null;  //消除強應用
    
  SoftReference<Object> sf =new SoftReference<Object>(obj);  //實現軟引用
  WeakReference<User> uwr = new WeakReference<User>(new User(1,"張三")); //實現弱引用

 面試題:開發中使用過WeakHashMap嗎?

  • WeakHashMap,用來儲存鍵值對(k-v)。 但是它是軟引用,即垃圾回收器執行的時候,就會回收該值,從而消除map中的資料
  • 比較適合做本地,堆內快取的儲存機制,快取的失效依賴於GC的行為

 

寄語:這個時代,認知升級遠比積累知識重要

相關文章