JVM垃圾回收機制

橡皮筋兒發表於2021-11-11

Java開發有個很基礎的問題,雖然我們平時接觸的不多,但是瞭解它卻成為Java開發的必備基礎——這就是JVM。
在Java中JVM內建了垃圾回收的機制,以守護程式的形式在後臺自動回收垃圾,它讓開發者無需關注空間的建立和釋放,幫助開發者承擔物件的建立和釋放的工作,極大的減輕了開發的負擔。那是不是我們就不需要了解JVM了,顯然在做一些優化或者深入研究應用效能的時候,JVM還是起了很關鍵的作用的。因此本篇就總結性的描述下垃圾回收相關的知識。

哪些記憶體需要回收

回收區域主要集中在java堆和方法區。
程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的,因此這幾個區域的記憶體分配和回收都具備確定性,所以不需要考慮回收,而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,垃圾收集器所關注的是這部分記憶體。

什麼時候回收

  • 物件沒有引用
  • 作用域發生未捕獲異常
  • 程式在作用域正常執行完畢
  • 程式執行了System.exit()
  • 程式發生意外終止(被殺程式等)

如何回收

所謂“垃圾”,就是指所有不再存活的物件。常見的判斷是否存活有兩種方法:引用計數法和可達性分析。

  • 引用計數法
    為每一個建立的物件分配一個引用計數器,用來儲存該物件被引用的個數。當該個數為零,意味著沒有人再使用這個物件,可以認為“物件死亡”。但是,這種方案存在嚴重的問題,就是無法檢測“迴圈引用”:當兩個物件互相引用,即時它倆都不被外界任何東西引用,它倆的計數都不為零,因此永遠不會被回收。而實際上對於開發者而言,這兩個物件已經完全沒有用處了。 因此,Java 裡沒有采用這樣的方案來判定物件的“存活性”。

  • 可達性分析
    這種方案是目前主流語言裡採用的物件存活性判斷方案。基本思路是把所有引用的物件想象成一棵樹,從樹的根結點 GC Roots 出發,持續遍歷找出所有連線的樹枝物件,這些物件則被稱為“可達”物件,或稱“存活”物件。其餘的物件則被視為“死亡”的“不可達”物件,或稱“垃圾”。 參考下圖,object5,object6 和 object7 便是不可達物件,視為“死亡狀態”,應該被垃圾回收器回收。

    可達性分析

    可作為GC root的物件
    我們可以猜測,GC Roots 本身一定是可達的,這樣從它們出發遍歷到的物件才能保證一定可達。那麼,Java 裡有哪些物件是一定可達呢?主要有以下四種:

    • 虛擬機器棧(幀棧中的本地變數表)中引用的物件。
    • 方法區中靜態屬性引用的物件。
    • 方法區中常量引用的物件。
    • 本地方法棧中 JNI 引用的物件。
      這裡只要知道有這麼幾種型別的 GC Roots,每次垃圾回收器會從這些根結點開始遍歷尋找所有可達節點。

垃圾回收演算法

上面已經知道,所有 GC Roots不可達的物件都稱為垃圾,參考下圖,黑色的表示垃圾,灰色表示存活物件,綠色表示空白空間。

image
那麼,我們如何來回收這些垃圾呢?

  • Mark-Sweep標記-清除演算法
    第一步,所謂“標記”就是利用可達性遍歷堆記憶體,把“存活”物件和“垃圾”物件進行標記,得到的結果如上圖;
    第二步,既然“垃圾”已經標記好了,那我們再遍歷一遍,把所有“垃圾”物件所佔的空間直接清空即可。結果如下:

    Mark-Sweep標記-清除演算法
    這便是“標記-清理”方案,簡單方便 ,但是容易產生記憶體碎片。

  • Mark-Compact標記-整理演算法
    既然上面的方法會產生記憶體碎片,那好,我在清理的時候,把所有 存活 物件扎堆到同一個地方,讓它們待在一起,這樣就沒有記憶體碎片了。 結果如下:

    Mark-Compact標記-整理演算法
    這兩種方案適合存活物件多,垃圾少的情況,它只需要清理掉少量的垃圾,然後挪動下存活物件就可以了。

  • Copying複製演算法
    這種方法比較粗暴,直接把堆記憶體分成兩部分,一段時間內只允許在其中一塊記憶體上進行分配,當這塊記憶體被分配完後,則執行垃圾回收,把所有存活物件全部複製到另一塊記憶體上,當前記憶體則直接全部清空。
    參考下圖:

    Copying複製演算法
    起初時只使用上面部分的記憶體,直到記憶體使用完畢,才進行垃圾回收,把所有存活物件搬到下半部分,並把上半部分進行清空。
    這種做法不容易產生碎片,也簡單粗暴;但是,它意味著你在一段時間內只能使用一部分的記憶體,超過這部分記憶體的話就意味著堆記憶體裡頻繁的 複製清空。
    這種方案適合 存活物件少,垃圾多 的情況,這樣在複製時就不需要複製多少物件過去,多數垃圾直接被清空處理。

  • Generational Collection 分代收集
    最後的這種方法是前面幾種的合體,即目前JVM主要採取的一種方法,思想就是把JVM分成不同的區域。每種區域使用不同的垃圾回收方法。

    分代收集

    上面可以看到堆分成三個區域:
    新生代(Young Generation):用於存放新建立的物件,採用複製回收方法,如果在s0和s1之間複製一定次數後,轉移到年老代中。這裡的垃圾回收叫做minor GC;
    年老代(Old Generation):這些物件垃圾回收的頻率較低,採用的標記整理方法,這裡的垃圾回收叫做major GC。
    永久代(Permanent Generation):存放Java本身的一些資料,當類不再使用時,也會被回收。

    這裡可以詳細的說一下新生代複製回收的演算法流程:
    在新生代中,分為三個區:Eden, from survivor, to survior。

    • 當觸發minor GC時,會先把Eden中存活的物件複製到to Survivor中;
    • 然後再看from survivor,如果次數達到年老代的標準,就複製到年老代中;如果沒有達到則複製到to - survivor中,如果to survivor滿了,則複製到年老代中。
    • 然後調換from survivor 和 to survivor的名字,保證每次to survivor都是空的等待物件複製到那裡的。

垃圾回收器

HotSpot 虛擬機器的垃圾收集器

  • 序列收集器 Serial
    這種收集器就是以單執行緒的方式收集,垃圾回收的時候其他執行緒也不能工作。

    序列收集器 Serial

  • 並行收集器 Parallel
    以多執行緒的方式進行收集

    並行收集器 Parallel

  • 併發標記清除收集器 Concurrent Mark Sweep Collector, CMS
    大致的流程為:初始標記--併發標記--重新標記--併發清除

    併發標記清除收集器 Concurrent Mark Sweep Collector, CMS

  • G1收集器 Garbage First Collector
    大致的流程為:初始標記--併發標記--最終標記--篩選回收

    G1收集器 Garbage First Collector

GC什麼時候觸發

由於物件進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種型別:Scavenge GC和Full GC。

  • Scavenge GC
    一般情況下,當新物件生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活物件,並且把尚且存活的物件移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分物件都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裡需要使用速度快、效率高的演算法,使Eden去能儘快空閒出來。

  • Full GC
    對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進行回收,所以比Scavenge GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於Full GC的調節。有如下原因可能導致Full GC:

    • 年老代(Tenured)被寫滿
    • 持久代(Perm)被寫滿
    • System.gc()被顯示呼叫
    • 上一次GC之後Heap的各域分配策略動態變化

參考連結:
www.importnew.com/26821.html
www.cnblogs.com/xing901022/…
www.cnblogs.com/1024Communi…
blog.csdn.net/sinat_33087…

相關文章