垃圾回收器(一)Serial、Parallel、CMS與三色標記詳解

引言

上篇文章咱們主要圍繞對象的建立過程展開描述,本篇文章咱們把思路切換到對象的回收,對於JVM的整個知識點而言,對象的回收纔是咱們真正要關心的。本篇涉及到的一些JVM參數比較多,詳細的能夠參考官方的解釋:docs.oracle.com/javase/8/do…html

垃圾回收算法

在目前Hotspot內部所實現的垃圾回收策略中,主要用到了3種垃圾回收算法:標記複製標記清除標記整理java

標記-清除

好比有一大塊內存空間,經過GC Roots 找到可用對象,將垃圾對象清除掉,把垃圾對象佔用的內存騰出來。假設在清理以前對象在內存中的佔用是這樣子的,黑色表示垃圾對象,藍色表示存活對象:算法

image.png 在使用標記清除算法進行一次GC以後,內存就變成了這樣子: image.png 圖中,白色部分表示垃圾對象被清理後留下來的可用內存,藍色表示存活對象。markdown

  • 缺點:
  1. 清除以後留下的內存不是規整的,存在大量的內存碎片,此時若是有一個大對象要分配進來的話,沒有能夠分配的內存,就會觸發一次GC。
  2. 在對象進行內存分配的時候,在內存不規整的狀況下,會使用空閒列表的方式,而這種方式會佔用一些內存。
  • 優勢:
  1. 僅需將垃圾對象清理掉,不用像其餘算法同樣作多餘的動做,效率比較高。
  2. 實現簡單。

標記-整理

對於標記清除作了一次優化,將有用的對象挪動到一邊,將另外一邊的垃圾對象清除掉。好比,清理以前是這樣子的,黑色是垃圾對象,藍色是有用對象: image.png GC以後的結果: image.png多線程

  • 優勢
  1. 解決了內存碎片的問題,避免因內存碎片產生的GC。
  • 缺點
  1. 由於要作一次內存的整理,這個過程當中涉及到內存地址的移動,性能稍差些。

標記-複製

好比將內存空間分爲A,B兩部分,A用來存放對象,B空着,回收的時候將A空間的根可達對象進行標記,將有用對象複製到B,再把A裏面的垃圾對象清理掉。 image.png併發

  • 缺點:
  1. 內存的佔用率低,有一部份內存會空着。
  2. 由於也會設計到內存地址的移動,存活對象不少的狀況下也會影響性能。
  • 優勢:
  1. 不存在內存碎片問題。
  2. 效率高,由於是批量清理一整塊內存,而不是找到具體的內存地址一個一個的清理。

分代收集理論

從上面的幾種垃圾回收算法中不難發現,每種方式都各有利弊,那麼JVM該經過什麼方式去權衡,選擇合適的回收算法呢:根據反覆的測試結果來看,Oracle官方發現98%的對象都是朝生夕死的,不須要佔用內存過久,存活率低;而剩餘的10%能夠理解爲頑固對象,要在內存中佔用好久,存活率比較高,基於這個因素,JVM把對象分開進行管理,也就是分代收集 ,將內存劃分爲年輕代和老年代;由於年輕代在執行幾分鐘或者幾秒後剩餘的對象不多,這種狀況下該用那種回收器呢,咱們一一分析:oracle

  • 標記-清除: 由於垃圾對象居多,清除的話會清除不少垃圾對象,對於性能會有影響。
  • 標記-整理: 比清除強不了多少,並且還附帶一次整理,性能也不行。
  • 複製: 僅需複製少許的存活對象,性能不會影響太大。

展開一通分析以後咱們發現,年輕代使用複製算法比較合適一些,可是上面咱們也說過他的弊端,空間利用率過小了,針對這種狀況JVM特地作了一種優化,引入了eden區域用來存放大量的對象,剩餘出來兩塊很小的空間用來存放複製算法後的存活對象,這樣咱們既能夠高效的回收,又能夠下降空間利用率小帶來的影響。然而老年代是通過幾回回收都存活的對象,很難再次被回收,基於這種垃圾對象不多的狀況下,清除和整理算法均可以作。因此如今出現了這樣的結果: image.pngoop

垃圾回收器

垃圾回收算法是一套回收理念,不一樣的垃圾回收器針對這套理念實現的時候會作一些優化,目前,Hotspot所實現的垃圾回收器有:Serial、Parallel、ParNew、CMS、G一、ZGC。不管是什麼樣的垃圾回收器,在回收垃圾的時候都會中止全部的業務線程,單獨讓GC線程進行垃圾回收(這個過程也被成爲STW),由於若是業務線程還在執行的話可能會打亂對象之間的引用關係,GC在進行標記的時候會混亂,因此必需要STW。 在JVM調優中,不管是年輕代仍是老年代,咱們的調優目的有兩個:1是避免OOM;2是減小STW的時間,讓用戶卡頓的感知減小。性能

Serial

爲了知足分代收集理念,Serial收集器分別在年輕代和老年代各實現了一個版本,老年代是Serial Old,使用標記整理算法。Serial在回收垃圾的整個過程當中都是採用單線程的方式,因此STW的時間會很長,內存越大,STW時間越長,用戶的卡頓時間就很長,如今咱們生產環境堆分配的通常都是幾個G以上的,因此Serial收集器必定會很慢很慢,在很早以前的jdk版本中有使用,如今都不主動用這個了,只有在使用CMS回收器併發清理失敗的狀況下系統會默認回退到這種方式。 image.png測試

  • 相關參數:

-XX:+UseSerialGC:使用Serial回收器。

Parallel

多線程的垃圾收集器,默認狀況下啓用的線程數是和CPU核心數相同。老年代是Parallel Old,使用標記整理算法,也是jdk1.8中使用的默認垃圾回收器。由於是使用多線程進行回收,因此STW的時間相對Serial來講會更短。這裏會涉及到一個吞吐量的概念:吞吐量 = 用戶應用程序運行的時間 / (應用程序運行的時間 + 垃圾回收的時間)。因此在Parallel收集器中,吞吐量是很高的,適用於追求吞吐量的系統。 image.png

  • 相關參數:

-XX:+UseParallelGC:開啓ParallelGC。
-XX:+UseParallelOldGC:開啓老年代的ParallelGC,和上面的任意開啓一個就行。
-XX:ParallelGCThreads:指定線程數。

咱們能夠經過參數:-XX:+PrintCommandLineFlags 將JVM的已經設置好的參數打印出來,發現他默認的GC就是Parallel: image.png

ParNew

和Parallel的實現基本同樣,惟一不一樣的是它能夠和CMS搭配使用,而Parallel不能夠,當設置了回收器是cms的時候,JVM則會默認開啓ParNew做爲年輕代的回收器且沒法關閉,對於Parallel的一些參數也能夠在ParNew裏面用。

CMS(Concurrent Mark Sweep)

儘管垃圾回收器從單線程發展到多線程,可是STW很長的問題始終是存在的,雖說Parllel的STW時長可能會短一點,但仍是沒有作到極致,在CMS中STW的停頓時間獲得了很好的解決:CMS在回收的過程當中容許和GC線程和用戶線程同時執行(並行)且將標記對象的過程延長,每次只標記一點點,以獲取最短回收停頓時間爲目標。同時,CMS也是垃圾收集器發展過程當中的轉折點,從CMS開始以後的垃圾回收器都是基於並行作GC的。這裏要注意:CMS是使用標記清除算法進行垃圾回收的。

  • CMS的垃圾回收過程主要分爲如下5步:
  1. 初始標記:記錄能被GC Root直接引用的對象,觸發一次STW,可是此次STW很快,由於在標記的過程當中不會標記一整條引用鏈的對象,如圖所示,只記錄紅色箭頭關聯到的對象,不記錄黑色箭頭。image.png
  2. 併發標記:從GC Roots的直接引用對象開始依次掃描(對上面的黑色箭頭的鏈路作掃描),這個過程可能須要好久,用戶線程和GC線程同時執行,不會產生STW,由於在掃描的過程當中用戶線程還在不斷的執行因此可能會出現被標記過的對象又變成了垃圾。
  3. 從新標記:這個過程會STW,主要是對併發標記所產生的浮動垃圾進行從新標記,比並發標記快不少。
  4. 併發清理:GC線程和用戶線程同時執行,將未標記的對象進行清理。此時若是出現新增對象則會把他進行標記,等待下次GC。
  5. 併發重置:重置本次GC過程當中的標記數據。

整個CMS的回收過程,能夠用一張圖來更清晰的看一下: image.png 在初始標記觸發STW的時候它的標記方式仍是原始的更改對象頭MarkWord的GC標記字段,可是在併發標記階段,由於是用戶線程和GC線程同時在跑,因此這裏採用的是三色標記的方式進行垃圾標記:

三色標記

將對象的標記過程分爲三種顏色:白色、灰色、黑色。

  • 白色:對象的默認顏色,從GC Root開始掃描,若是是不可達對象的話就是白色,也就是垃圾對象,在併發清理的時候會清理掉。
  • 灰色:當前對象已經被掃描過,可是當前對象所依賴到的其餘對象尚未被掃描。
  • 黑色:當前對象和他所依賴的對象都已經被掃描過。

可是,這種方式會存在漏標多標的問題:

漏標

好比如今有ABCD四個對象,A依賴了B和C,C依賴了D;初始標記完以後A對象已經被掃描過了因此是灰色,其餘對象是白色:
image.png
繼續往下執行掃描B和C,當B和C掃描完以後,A變成了黑色,B變成了灰色,C是黑色,D仍是白色:
image.png
此時若是用戶線程把B和D的引用去掉,讓C依賴D,創建起C和D的關係以後B變成了黑色:
image.png
那麼問題來了,C已是黑色就不回再對其依賴對象掃描了,但事實上C還有一個依賴對象D沒有被掃描。此時若是進行垃圾回收的話D會被回收掉,這就是所謂的漏標問題。

多標

還用上面的例子說,好比如今AB是黑色,C是灰色,D是白色,當GC正在掃描D的時候,B被置空了,從邏輯上來說B是垃圾,理應被回收,可是由於GC不會對黑色對象作重複掃描因此B仍是黑色,在垃圾清理的時候B不會被回收,只能等到下次GC的時候再從新進行標記掃描。這種狀況相對於漏標來講還行,起碼不會致使系統出BUG。

漏標的解決方案

  • 增量更新

將新增的引用維護到一個集合裏面,將引用的源頭變爲灰色,等待從新標記階段在從新進行一次掃描。 好比:當D的引用指向了C,則會將C變爲灰色,並將C放到一個新增引用的集合裏面;在從新標記階段會將C做爲根節開始繼續向下掃描。

爲何CMS要使用標記清除

CMS的垃圾回收階段是併發回收的,若是使用標記整理的話,對象的內存地址會進行移動,由於用戶線程還在執行,爲了不因內存地址移動而帶來的bug,還須要對用戶線程的對象指針進行維護,在這個過程當中確定會STW,這樣作就提升了垃圾清理的時長,停頓時間也變長了,不符合CMS以獲取最短回收停頓時間爲目標的設計初衷。

小結

CMS的回收週期很長,可是他的STW時間是分開的,好比總的STW要100ms,可能他會在初始標記消耗20ms,從新標記消耗80ms,對於用戶來講能感知的到停頓時長可能只有80ms,也就是說CMS的設計初衷是爲了提升用戶體驗,減小停頓時間。這是和Parallel最大的不一樣。正由於CMS的回收週期很長,因此在垃圾不少的狀況下可能出現上次的GC週期還沒執行完就又觸發了GC,被稱爲」concurrent mode failure「;對於這種狀況會回退到Serial的方式進行回收,全程STW。由於是採用標記清除算法,因此會存在內存碎片的問題,經過參數-XX:+UseCMSCompactAtFullCollection 能夠設置清除以後再作一次整理。

  • CMS相關參數

-XX:+UseConcMarkSweepGC:使用CMS垃圾收集器,當設置這個參數後,年輕代默認會開啓ParNew。
-XX:ConcGCThreads:併發的GC線程數,默認是CPU的核數。
-XX:+UseCMSCompactAtFullCollection:至關於標記整理。
-XX:CMSFullGCsBeforeCompaction:多少次FullGC以後壓縮一次,默認是0。
-XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC,默認是92。
-XX:+UseCMSInitiatingOccupancyOnly:這個參數搭配上面那個用,表示是否是要一直使用上面的比例觸發FullGC,若是設置則只會在第一次FullGC的時候使用-XX:CMSInitiatingOccupancyFraction的值,以後會進行自動調整。
-XX:+CMSScavengeBeforeRemark:在FullGC前啓動一次MinorGC,目的在於減小老年代對年輕代的引用,下降CMS GC的標記階段時的開銷,通常CMS的GC耗時80%都在標記階段。 -XX:+CMSParallellnitialMarkEnabled:默認狀況下初始標記是單線程的,這個參數可讓他多線程執行,能夠減小STW。
-XX:+CMSParallelRemarkEnabled:使用多線程進行從新標記,目的也是爲了減小STW。

日日行,不怕千萬裏;經常作,不怕千萬事。

相關文章
相關標籤/搜索