JVM系列(2)-GC

  • 2020 年 5 月 18 日
  • 筆記

1.什麼是GC?

大白話說就是垃圾回收機制,內存空間是有限的,你創建的每個對象和變量都會佔據內存,gc做的就是對象清除將內存釋放出來,這就是GC要做的事。

2.需要GC的區域

說起垃圾回收的場所,了解過JVM(Java Virtual Machine Model)內存模型的朋友應該會很清楚,堆是Java虛擬機進行垃圾回收的主要場所,其次要場所是方法區。

3.堆內存的結構

Java將堆內存分為3大部分:新生代、老年代和永久代,其中新生代又進一步劃分為Eden、S0、S1(Survivor)三個區

4.堆內存上對象的分配與回收:

我們創建的對象會優先在Eden分配,如果是大對象(很長的字符串數組)則可以直接進入老年代。虛擬機提供一個
-XX:PretenureSizeThreadhold參數,令大於這個參數值的對象直接在老年代中分配,避免在Eden區和兩個Survivor區發生大量的內存拷貝。

另外,長期存活的對象將進入老年代,每一次MinorGC(年輕代GC),對象年齡就大一歲,默認15歲晉陞到老年代,通過
-XX:MaxTenuringThreshold設置晉陞年齡。

堆內存上的對象回收也叫做垃圾回收,那麼垃圾回收什麼時候開始呢?

垃圾回收主要是完成清理對象,整理內存的工作。上面說到GC經常發生的區域是堆區,堆區還可以細分為新生代、老年代。新生代還分為一個Eden區和兩個Survivor區。垃圾回收分為年輕代區域發生的Minor GC和老年代區域發生的Full GC,分別介紹如下。

Minor GC(年輕代GC):
對象優先在Eden中分配,當Eden中沒有足夠空間時,虛擬機將發生一次Minor GC,因為Java大多數對象都是朝生夕滅,所以Minor GC非常頻繁,而且速度也很快。

Full GC(老年代GC):
Full GC是指發生在老年代的GC,當老年代沒有足夠的空間時即發生Full GC,發生Full GC一般都會有一次Minor GC。

接下來,我們來看關於內存分配與回收的兩個重要概念吧。

動態對象年齡判定:

如果Survivor空間中相同年齡所有對象的大小總和大於Survivor空間的一半,那麼年齡大於等於該對象年齡的對象即可晉陞到老年代,不必要等到-XX:MaxTenuringThreshold。

空間分配擔保:

發生Minor GC時,虛擬機會檢測之前每次晉陞到老年代的平均大小是否大於老年代的剩餘空間大小。如果大於,則進行一次Full GC(老年代GC),如果小於,則查看HandlePromotionFailure設置是否允許擔保失敗,如果允許,那隻會進行一次Minor GC,如果不允許,則改為進行一次Full GC

5.目前會問到的問題

1.年輕代三個區比例

Eden,S0,S1比例8:1:1

2.為什麼要有Survivor區

這個我用別人說的話解釋一下:

鏈接://www.jianshu.com/p/2caad185ee1f

為什麼需要Survivor空間。我們看看如果沒有 Survivor 空間的話,垃圾收集將會怎樣進行:一遍新生代 gc 過後,不管三七二十一,活着的對象全部進入老年代,即便它在接下來的幾次 gc 過程中極有可能被回收掉。這樣的話老年代很快被填滿, Full GC 的頻率大大增加。我們知道,老年代一般都會被規劃成比新生代大很多,對它進行垃圾收集會消耗比較長的時間;如果收集的頻率又很快的話,那就更糟糕了。基於這種考慮,虛擬機引進了「倖存區」的概念:如果對象在某次新生代 gc 之後任然存活,讓它暫時進入倖存區;以後每熬過一次 gc ,讓對象的年齡+1,直到其年齡達到某個設定的值(比如15歲), JVM 認為它很有可能是個「老不死的」對象,再呆在倖存區沒有必要(而且老是在兩個倖存區之間反覆地複製也需要消耗資源),才會把它轉移到老年代。

Survivor的存在意義,就是減少被送到老年代的對象,進而減少Full GC的發生,Survivor的預篩選保證,只有經歷16次Minor GC還能在新生代中存活的對象,才會被送到老年代。

3.為什麼有兩個Survivor區

為什麼 Survivor 分區不能是 0 個?

如果 Survivor 是 0 的話,也就是說新生代只有一個 Eden 分區,每次垃圾回收之後,存活的對象都會進入老生代,這樣老生代的內存空間很快就被佔滿了,從而觸發最耗時的 Full GC ,顯然這樣的收集器的效率是我們完全不能接受的。

為什麼 Survivor 分區不能是 1 個?

如果 Survivor 分區是 1 個的話,假設我們把兩個區域分為 1:1,那麼任何時候都有一半的內存空間是閑置的,顯然空間利用率太低不是最佳的方案。

但如果設置內存空間的比例是 8:2 ,只是看起來似乎「很好」,假設新生代的內存為 100 MB( Survivor 大小為 20 MB ),現在有 70 MB 對象進行垃圾回收之後,剩餘活躍的對象為 15 MB 進入 Survivor 區,這個時候新生代可用的內存空間只剩了 5 MB,這樣很快又要進行垃圾回收操作,顯然這種垃圾回收器最大的問題就在於,需要頻繁進行垃圾回收。

為什麼 Survivor 分區是 2 個?

剛剛新建的對象在Eden中,經歷一次Minor GC,Eden中的存活對象就會被移動到第一塊survivor space S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和S0中的存活對象又會被複制送入第二塊survivor space S1(這個過程非常重要,因為這種複製算法保證了S1中來自S0和Eden兩部分的存活對象佔用連續的內存空間,避免了碎片化的發生)。S0和Eden被清空,然後下一輪S0與S1交換角色,如此循環往複。如果對象的複製次數達到16次,該對象就會被送到老年代中。下圖中每部分的意義和上一張圖一樣,就不加註釋了。
兩塊Survivor避免碎片化
上述機制最大的好處就是,整個過程中,永遠有一個survivor space是空的,另一個非空的survivor space無碎片

那麼,Survivor為什麼不分更多塊呢?比方說分成三個、四個、五個?顯然,如果Survivor區再細分下去,每一塊的空間就會比較小,很容易導致Survivor區滿

總結

根據上面的分析可以得知,當新生代的 Survivor 分區為 2 個的時候,不論是空間利用率還是程序運行的效率都是最優的,所以這也是為什麼 Survivor 分區是 2 個的原因了。

6. JVM如何判定一個對象是否應該被回收?(重點掌握)

 判斷一個對象是否應該被回收,主要是看其是否還有引用。判斷對象是否存在引用關係的方法包括引用計數法以及可達性分析

引用計數法:

是一種比較古老的回收算法。原理是此對象有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只需要收集計數為0的對象。此算法最致命的是無法處理循環引用的問題。

可達性分析

可達性分析的基本思路就是通過一系列可以做為root的對象作為起始點,從這些節點開始向下搜索。當一個對象到root節點沒有任何引用鏈接時,則證明此對象是可以被回收的。以下對象會被認為是root對象:

  • 棧內存中引用的對象 
  • 方法區中靜態引用和常量引用指向的對象 
  • 被啟動類(bootstrap加載器)加載的類和創建的對象
  • Native方法中JNI引用的對象。 

7. JVM垃圾回收算法有哪些?

HotSpot 虛擬機採用了可達性分析來進行內存回收,常見的回收算法有標記-清除算法,複製算法和標記整理算法。

標記-清除算法(Mark-Sweep):

標記-清除算法執行分兩階段。

第一階段:從引用根節點開始標記所有被引用的對象,

第二階段:遍歷整個堆,把未標記的對象清除。此算法需要暫停整個應用,並且會產生內存碎片。

 

 

 缺點:

  • 執行效率不穩定,會因為對象數量增長,效率變低
  • 標記清除後會有大量的不連續的內存碎片,空間碎片太多就會導致無法分配較大對象,無法找到足夠大的連續內存,而發生gc

複製算法:

複製算法把內存空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另外一個區域中。複製算法每次只處理正在使用中的對象,因此複製成本比較小,同時複製過去以後還能進行相應的內存整理,不會出現「碎片」問題。當然,此算法的缺點也是很明顯的,就是需要兩倍內存空間。

 

 

 缺點:

  • 可用內存縮成了一半,浪費空間

標記-整理算法:

標記-整理算法結合了「標記-清除」和「複製」兩個算法的優點。也是分兩階段,

第一階段從根節點開始標記所有被引用對象,

第二階段遍歷整個堆,清除未標記對象並且把存活對象「壓縮」到堆的其中一塊,按順序排放。此算法避免了「標記-清除」的碎片問題,同時也避免了「複製」算法的空間問題。

 

 

 

8.垃圾收集器(掌握CMS和G1)

JVM中的垃圾收集器主要包括7種,即Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old以及CMS,G1收集器。如下圖所示:

 

 

1、Serial收集器:

Serial收集器是一個單線程的垃圾收集器,並且在執行垃圾回收的時候需要 Stop The World。虛擬機運行在Client模式下的默認新生代收集器。Serial收集器的優點是簡單高效,對於限定在單個CPU環境來說,Serial收集器沒有多線程交互的開銷。

2、Serial Old收集器:

Serial Old是Serial收集器的老年代版本,也是一個單線程收集器。主要也是給在Client模式下的虛擬機使用。在Server模式下存在主要是做為CMS垃圾收集器的後備預案,CMS並發收集發生Concurrent Mode Failure時使用。

3、ParNew收集器:

ParNew是Serial收集器的多線程版本,新生代是並行的(多線程的),老年代是串行的(單線程的),新生代採用複製算法,老年代採用標記整理算法。可以使用參數:-XX:UseParNewGC使用該收集器,使用 -XX:ParallelGCThreads可以限制線程數量。

4、Parallel Scavenge垃圾收集器:

Parallel Scavenge是一種新生代收集器,使用複製算法的收集器,而且是並行的多線程收集器。Paralle收集器特點是更加關注吞吐量(吞吐量就是cpu用於運行用戶代碼的時間與cpu總消耗時間的比值)。可以通過-XX:MaxGCPauseMillis參數控制最大垃圾收集停頓時間;通過-XX:GCTimeRatio參數直接設置吞吐量大小;通過-XX:+UseAdaptiveSizePolicy參數可以打開GC自適應調節策略,該參數打開之後虛擬機會根據系統的運行情況收集性能監控信息,動態調整虛擬機參數以提供最合適的停頓時間或者最大的吞吐量。自適應調節策略Parallel Scavenge收集器和ParNew的主要區別之一。

5、Parallel Old收集器:

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法。

6、CMS(Concurrent Mark Sweep)收集器(並發標記清除)

CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS收集器是基於標記-清除算法實現的,是一種老年代收集器,通常與ParNew一起使用。

CMS的垃圾收集過程分為4步:

  • 初始標記:需要「Stop the World」,初始標記僅僅只是標記一下GC Root能直接關聯到的對象,速度很快。
  • 並發標記:是主要標記過程,這個標記過程是和用戶線程並發執行的。
  • 重新標記:需要「Stop the World」,為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄(停頓時間比初始標記長,但比並發標記短得多)。
  • 並發清除:和用戶線程並發執行的,基於標記結果來清理對象。

 

 

 

那麼問題來了,如果在重新標記之前剛好發生了一次MinorGC,會不會導致重新標記階段Stop the World時間太長?

答:不會的,在並發標記階段其實還包括了一次並發的預清理階段,虛擬機會主動等待年輕代發生垃圾回收,這樣可以將重新標記對象引用關係的步驟放在並發標記階段,有效降低重新標記階段Stop The World的時間。

CMS垃圾回收器的優缺點分析:

CMS以降低垃圾回收的停頓時間為目的,很顯然其具有並發收集,停頓時間低的優點。

缺點主要包括如下:

  • CPU資源非常敏感,因為並發標記和並發清理階段和用戶線程一起運行,當CPU數變小時,性能容易出現問題。
  • 收集過程中會產生浮動垃圾,所以不可以在老年代內存不夠用了才進行垃圾回收,必須提前進行垃圾收集。通過參數-XX:CMSInitiatingOccupancyFraction的值來控制內存使用百分比。如果該值設置的太高,那麼在CMS運行期間預留的內存可能無法滿足程序所需,會出現Concurrent Mode Failure失敗,之後會臨時使用Serial Old收集器做為老年代收集器,會產生更長時間的停頓。
  • 標記-清除方式會產生內存碎片,可以使用參數-XX:UseCMSCompactAtFullCollection來控制是否開啟內存整理(無法並發,默認是開啟的)。參數-XX:CMSFullGCsBeforeCompaction用於設置執行多少次不壓縮的Full GC後進行一次帶壓縮的內存碎片整理(默認值是0)。

接下來,我們先看下上邊介紹的浮動垃圾是怎麼產生的吧。

浮動垃圾:

由於在應用運行的同時進行垃圾回收,所以有些垃圾可能在垃圾回收進行完成時產生,這樣就造成了「Floating Garbage」,這些垃圾需要在下次垃圾回收周期時才能回收掉。所以,並發收集器一般需要20%的預留空間用於這些浮動垃圾。

7、G1(Garbage-First)收集器:

G1收集器將新生代和老年代取消了,取而代之的是將堆劃分為若干的區域,每個區域都可以根據需要扮演新生代的Eden和Survivor區或者老年代空間,仍然屬於分代收集器,區域的一部分包含新生代,新生代採用複製算法,老年代採用標記-整理算法。

通過JVM堆分為一個個的區域(region),G1收集器可以避免在Java堆中進行全區域的垃圾收集。G1跟蹤各個region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後台維護一個優先列表,每次根據回收時間來優先回收價值最大的region。

G1收集器的特點:

  • 並行與並發G1能充分利用多CPU,多核環境下的硬件優勢,來縮短Stop the World,是並發的收集器。
  • 分代收集G1不需要其他收集器就能獨立管理整個GC堆,能夠採用不同的方式去處理新建對象、存活一段時間的對象和熬過多次GC的對象。
  • 空間整合G1從整體來看是基於標記-整理算法,從局部(兩個Region)上看基於複製算法實現,G1運作期間不會產生內存空間碎片。
  • 可預測的停頓:能夠建立可以預測的停頓時間模型,預測停頓時間。

CMS收集器類似,G1收集器的垃圾回收工作也分為了四個階段:

  • 初始標記
  • 並發標記
  • 最終標記
  • 篩選回收

其中,篩選回收階段首先對各個Region的回收價值和成本進行計算,根據用戶期望的GC停頓時間來制定回收計劃。

9.Java常用版本垃圾收集器

首先說如果看怎麼看

我的版本是jdk1.8

java -XX:+PrintCommandLineFlags -version

 

 

Parallel Scavenge垃圾收集器管理的新生代,Parallel Old管理的老年代

jdk1.9 默認垃圾收集器G1