1. 程式人生 > 其它 >JVM系列3-垃圾回收演算法

JVM系列3-垃圾回收演算法

上篇文章中我們瞭解了Java記憶體模型,並提到了垃圾回收,那怎麼確定一個物件是垃圾,又是如何回收這些垃圾的呢?

如何確定一個物件是垃圾

要想進行垃圾回收,得先知道什麼樣的物件是垃圾。有兩種方式

  • 引用計數法
    對於某個物件而言,只要應用程式中持有該物件的引用,就說明該物件不是垃圾,如果一個物件沒有任何指標對其引用,它就是垃圾。
    弊端:如果AB相互持有引用,導致永遠不能被回收。

  • 可達性分析
    通過GC Root的物件,開始向下尋找,看某個物件是否可達

那哪些物件可以作為GC ROOT?

類載入器、Thread、虛擬機器棧的本地變量表、static成員、常量引用、本地方法、棧的變數等。

垃圾回收演算法

已經能夠確定一個物件為垃圾之後,接下來要考慮的就是回收,怎麼回收呢?
得要有對應的演算法,下面聊聊常見的垃圾回收演算法。

標記-清除(Mark-Sweep)

  • 標記:根據GC ROOT標記物件,分為可達物件、沒有引用的物件、未使用的空間

  • 清除: 清除掉被標記需要回收的物件(沒有引用的物件 ),釋放出對應的記憶體空間

問題:

  1. 標記和清除兩個過程都比較耗時,堆中所有的物件都會被掃描一遍,從而才能確定需要回收的物件,效率不高
  2. 會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作

複製演算法(Copying)

將記憶體劃分為兩塊相等的區域,每次只使用其中一塊。當其中一塊記憶體使用完了,就將還存活的物件複製到另外一塊上面,然後把已經使用過的記憶體空間一次清除掉。

優勢: 空間連續
問題: 空間浪費

標記-整理(Mark-Compact)

標記過程仍然與"標記-清除"演算法一樣,但是後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

分代收集演算法

既然上面介紹了3中垃圾收集演算法,那麼在堆記憶體中到底用哪一個呢?

  • Young區:複製演算法(物件在被分配之後,可能生命週期比較短,Young區複製效率比較高)
  • Old區:標記清除或標記整理(Old區物件存活時間比較長,複製來複制去沒必要,不如做個標記再清理)

垃圾收集器

收集演算法是記憶體回收的方法論,垃圾收集器就是記憶體回收的具體實現

新生代:適用於少量物件存活的場景, 所有采用複製演算法。有3種垃圾收集器

  • Serial : 適合單核CPU機器 單執行緒處理垃圾回收,會STW(stop the wrold)
  • ParNew: 適合多核CPU機器 並行處理垃圾回收
  • Paraller Scavenge: 類似ParNew 但是更關注吞吐量(業務執行時間/業務執行時間 + 垃圾回收時間)

老年代:採用標記清除 / 標記整理演算法

  • Serial Old
  • Paraller Old
  • CMS (Concurrent mark sweep ):併發類的垃圾收集器 ,關注停頓時間(業務執行緒和垃圾回收執行緒可以一起執行)

G1垃圾收集器橫跨新生代和老年代,G1物理上不將記憶體分為新生代、老年代、S0、S1、Eden,而是劃分為一個個區(region), G1收集器可以設定停頓時間,在這個時間內將盡可能收集多的垃圾

分類

  1. 序列收集器->Serial和Serial Old
    只能有一個垃圾回收執行緒執行,使用者執行緒暫停。 適用於記憶體比較小的嵌入式裝置 。

  2. 並行收集器[吞吐量優先]->Parallel Scanvenge、Parallel Old
    多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態。 適用於科學計算、後臺處理等若互動場 景 。

  3. 併發收集器[停頓時間優先]->CMS、G1
    使用者執行緒和垃圾收集執行緒同時執行(但並不一定是並行的,可能是交替執行的),垃圾收集執行緒在執行的時候不會停頓使用者執行緒的執行。 適用於相對時間有要求的場景,比如Web

吞吐量和停頓時間

  • 停頓時間->垃圾收集器 進行 垃圾回收終端應用執行響應的時間

  • 吞吐量->執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)

停頓時間越短就越適合需要和使用者互動的程式,良好的響應速度能提升使用者體驗; 高吞吐量則可以高效地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任 務。

如何選擇合適的垃圾收集器

官網 :https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28

  • 優先調整堆的大小讓伺服器自己來選擇
  • 如果記憶體小於100M,使用序列收集器
  • 如果是單核,並且沒有停頓時間要求,使用序列或JVM自己選
  • 如果允許停頓時間超過1秒,選擇並行或JVM自己選
  • 如果響應時間最重要,並且不能超過1秒,使用併發收集器

G1收集器

JDK 7開始使用,JDK 8非常成熟,JDK 9預設的垃圾收集器,適用於新老生代。

判斷是否需要使用G1收集器?

  1. 50%以上的堆被存活物件佔用
  2. 物件分配和晉升的速度變化非常大
  3. 垃圾回收時間比較長

如何開啟需要的垃圾收集器

設定JVM引數

  • 序列
 -XX:+UseSerialGC
 -XX:+UseSerialOldGC
  • 並行(吞吐量優先):
 -XX:+UseParallelGC
 -XX:+UseParallelOldGC
  • 併發收集器(響應時間優先)
-XX:+UseConcMarkSweepGC 
-XX:+UseG1G