1. 程式人生 > >JVM系列(2)-GC

JVM系列(2)-GC

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區

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

連結:https://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 分割槽不能是 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 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常用版本垃圾收集器

1.首先說如果看怎麼看

我的版本是jdk1.8

java -XX:+PrintCommandLineFlags -version

2.jdk1.8和1.9用的版本

jdk1.8預設的新生代垃圾收集器:Parallel Scavenge,老年代:Parallel Old

jdk1.9 預設垃圾收集器G1