上篇文章咱們主要圍繞對象的建立過程展開描述,本篇文章咱們把思路切換到對象的回收,對於JVM的整個知識點而言,對象的回收纔是咱們真正要關心的。本篇涉及到的一些JVM參數比較多,詳細的能夠參考官方的解釋:docs.oracle.com/javase/8/do…html
在目前Hotspot內部所實現的垃圾回收策略中,主要用到了3種垃圾回收算法:標記複製
、標記清除
、標記整理
。java
好比有一大塊內存空間,經過GC Roots 找到可用對象,將垃圾對象清除掉,把垃圾對象佔用的內存騰出來。假設在清理以前對象在內存中的佔用是這樣子的,黑色表示垃圾對象,藍色表示存活對象:算法
在使用標記清除算法進行一次GC以後,內存就變成了這樣子:
圖中,白色部分表示垃圾對象被清理後留下來的可用內存,藍色表示存活對象。markdown
對於標記清除作了一次優化,將有用的對象挪動到一邊,將另外一邊的垃圾對象清除掉。好比,清理以前是這樣子的,黑色是垃圾對象,藍色是有用對象: GC以後的結果:
多線程
好比將內存空間分爲A,B兩部分,A用來存放對象,B空着,回收的時候將A空間的根可達對象進行標記,將有用對象複製到B,再把A裏面的垃圾對象清理掉。 併發
從上面的幾種垃圾回收算法中不難發現,每種方式都各有利弊,那麼JVM該經過什麼方式去權衡,選擇合適的回收算法呢:根據反覆的測試結果來看,Oracle官方發現98%的對象都是朝生夕死的,不須要佔用內存過久,存活率低;而剩餘的10%能夠理解爲頑固對象,要在內存中佔用好久,存活率比較高,基於這個因素,JVM把對象分開進行管理,也就是分代收集 ,將內存劃分爲年輕代和老年代;由於年輕代在執行幾分鐘或者幾秒後剩餘的對象不多,這種狀況下該用那種回收器呢,咱們一一分析:oracle
展開一通分析以後咱們發現,年輕代使用複製算法比較合適一些,可是上面咱們也說過他的弊端,空間利用率過小了,針對這種狀況JVM特地作了一種優化,引入了eden區域用來存放大量的對象,剩餘出來兩塊很小的空間用來存放複製算法後的存活對象,這樣咱們既能夠高效的回收,又能夠下降空間利用率小帶來的影響。然而老年代是通過幾回回收都存活的對象,很難再次被回收,基於這種垃圾對象不多的狀況下,清除和整理算法均可以作。因此如今出現了這樣的結果: oop
垃圾回收算法是一套回收理念,不一樣的垃圾回收器針對這套理念實現的時候會作一些優化,目前,Hotspot所實現的垃圾回收器有:Serial、Parallel、ParNew、CMS、G一、ZGC。不管是什麼樣的垃圾回收器,在回收垃圾的時候都會中止全部的業務線程,單獨讓GC線程進行垃圾回收(這個過程也被成爲STW),由於若是業務線程還在執行的話可能會打亂對象之間的引用關係,GC在進行標記的時候會混亂,因此必需要STW。 在JVM調優中,不管是年輕代仍是老年代,咱們的調優目的有兩個:1是避免OOM;2是減小STW的時間,讓用戶卡頓的感知減小。性能
爲了知足分代收集理念,Serial收集器分別在年輕代和老年代各實現了一個版本,老年代是Serial Old,使用標記整理算法。Serial在回收垃圾的整個過程當中都是採用單線程的方式,因此STW的時間會很長,內存越大,STW時間越長,用戶的卡頓時間就很長,如今咱們生產環境堆分配的通常都是幾個G以上的,因此Serial收集器必定會很慢很慢,在很早以前的jdk版本中有使用,如今都不主動用這個了,只有在使用CMS回收器併發清理失敗的狀況下系統會默認回退到這種方式。 測試
-XX:+UseSerialGC
:使用Serial回收器。
多線程的垃圾收集器,默認狀況下啓用的線程數是和CPU核心數相同。老年代是Parallel Old,使用標記整理算法,也是jdk1.8中使用的默認垃圾回收器。由於是使用多線程進行回收,因此STW的時間相對Serial來講會更短。這裏會涉及到一個吞吐量的概念:吞吐量 = 用戶應用程序運行的時間 / (應用程序運行的時間 + 垃圾回收的時間)。因此在Parallel收集器中,吞吐量是很高的,適用於追求吞吐量的系統。
-XX:+UseParallelGC
:開啓ParallelGC。
-XX:+UseParallelOldGC
:開啓老年代的ParallelGC,和上面的任意開啓一個就行。
-XX:ParallelGCThreads
:指定線程數。
咱們能夠經過參數:-XX:+PrintCommandLineFlags
將JVM的已經設置好的參數打印出來,發現他默認的GC就是Parallel:
和Parallel的實現基本同樣,惟一不一樣的是它能夠和CMS搭配使用,而Parallel不能夠,當設置了回收器是cms的時候,JVM則會默認開啓ParNew做爲年輕代的回收器且沒法關閉,對於Parallel的一些參數也能夠在ParNew裏面用。
儘管垃圾回收器從單線程發展到多線程,可是STW很長的問題始終是存在的,雖說Parllel的STW時長可能會短一點,但仍是沒有作到極致,在CMS中STW的停頓時間獲得了很好的解決:CMS在回收的過程當中容許和GC線程和用戶線程同時執行(並行)且將標記對象的過程延長,每次只標記一點點,以獲取最短回收停頓時間爲目標。同時,CMS也是垃圾收集器發展過程當中的轉折點,從CMS開始以後的垃圾回收器都是基於並行作GC的。這裏要注意:CMS是使用標記清除算法進行垃圾回收的。
整個CMS的回收過程,能夠用一張圖來更清晰的看一下: 在初始標記觸發STW的時候它的標記方式仍是原始的更改對象頭MarkWord的GC標記字段,可是在併發標記階段,由於是用戶線程和GC線程同時在跑,因此這裏採用的是三色標記的方式進行垃圾標記:
將對象的標記過程分爲三種顏色:白色、灰色、黑色。
可是,這種方式會存在漏標與多標的問題:
好比如今有ABCD四個對象,A依賴了B和C,C依賴了D;初始標記完以後A對象已經被掃描過了因此是灰色,其餘對象是白色:
繼續往下執行掃描B和C,當B和C掃描完以後,A變成了黑色,B變成了灰色,C是黑色,D仍是白色:
此時若是用戶線程把B和D的引用去掉,讓C依賴D,創建起C和D的關係以後B變成了黑色:
那麼問題來了,C已是黑色就不回再對其依賴對象掃描了,但事實上C還有一個依賴對象D沒有被掃描。此時若是進行垃圾回收的話D會被回收掉,這就是所謂的漏標問題。
還用上面的例子說,好比如今AB是黑色,C是灰色,D是白色,當GC正在掃描D的時候,B被置空了,從邏輯上來說B是垃圾,理應被回收,可是由於GC不會對黑色對象作重複掃描因此B仍是黑色,在垃圾清理的時候B不會被回收,只能等到下次GC的時候再從新進行標記掃描。這種狀況相對於漏標來講還行,起碼不會致使系統出BUG。
將新增的引用維護到一個集合裏面,將引用的源頭變爲灰色,等待從新標記階段在從新進行一次掃描。 好比:當D的引用指向了C,則會將C變爲灰色,並將C放到一個新增引用的集合裏面;在從新標記階段會將C做爲根節開始繼續向下掃描。
CMS的垃圾回收階段是併發回收的,若是使用標記整理的話,對象的內存地址會進行移動,由於用戶線程還在執行,爲了不因內存地址移動而帶來的bug,還須要對用戶線程的對象指針進行維護,在這個過程當中確定會STW,這樣作就提升了垃圾清理的時長,停頓時間也變長了,不符合CMS以獲取最短回收停頓時間爲目標的設計初衷。
CMS的回收週期很長,可是他的STW時間是分開的,好比總的STW要100ms,可能他會在初始標記消耗20ms,從新標記消耗80ms,對於用戶來講能感知的到停頓時長可能只有80ms,也就是說CMS的設計初衷是爲了提升用戶體驗,減小停頓時間。這是和Parallel最大的不一樣。正由於CMS的回收週期很長,因此在垃圾不少的狀況下可能出現上次的GC週期還沒執行完就又觸發了GC,被稱爲」concurrent mode failure「;對於這種狀況會回退到Serial的方式進行回收,全程STW。由於是採用標記清除算法,因此會存在內存碎片的問題,經過參數-XX:+UseCMSCompactAtFullCollection
能夠設置清除以後再作一次整理。
-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。
日日行,不怕千萬裏;經常作,不怕千萬事。