上篇介紹了一些經典的垃圾收集算法和它們的優缺點 垃圾回收算法看這一篇就夠了 今天跟着顧南的腳步,咱們一塊兒看一下HotSpot虛擬機中爲了實現垃圾收集作了哪些事情,而且瞭解幾個經典垃圾收集器的原理和適用場景,最後咱們學會看gc日誌,以及如何編寫高質量的代碼來優化垃圾收集器行爲,話很少說咱們開始;java
垃圾收集器要決定三件事web
因爲引用技術的循環引用等一些問題,jvm中都是使用的追蹤式的垃圾收集器,那它們是如何判斷一個對象是否要回收的呢,答案是依靠根結點枚舉和可達性分析,在一些低延遲的垃圾回收器中(好比cms),可達性分析能夠與用戶線程併發進行,而不用停頓。算法
併發和並行的區別數組
並行(Parallel):並行描述的是多條垃圾收集器線程之間的關係,說明同一時間有多條這樣的線 程在協同工做,一般默認此時用戶線程是處於等待狀態。 併發(Concurrent):併發描述的是垃圾收集器線程與用戶線程之間的關係,說明同一時間垃圾 收集器線程與用戶線程都在運行。因爲用戶線程並未被凍結,因此程序仍然能響應服務請求,但因爲 垃圾收集器線程佔用了一部分系統資源,此時應用程序的處理的吞吐量將受到必定影響。瀏覽器
可達性分析算法是從離散數學中的圖論引入的,程序把全部的引用關係看做一張圖,經過一系列的名爲 「GC Roots」 的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain)。當一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來講就是從 GC Roots 到這個對象不可達)時,則證實此對象是不可用的,以下圖所示。在Java中,可做爲 GC Root 的對象包括如下幾種:安全
這些根結點可能會隨着程序的運行不斷的變化,那收集器的根結點枚舉這一步驟時都是必須暫停用戶線程的,會面臨stop th world的問題,在大多數web系統中,終端全部用戶線程會給用戶帶來很差的體驗,即便是號稱不會停頓的cms,g1,zgc等收集器,在根結點枚舉這一步驟,也是須要停頓的。 在HotSpot虛擬機中,使用一組稱爲OopMap的數據結構來解決這一問題的,在程序運行到全局安全點時,虛擬機能夠能夠在OopMap的協助下快速的實現先根結點枚舉服務器
安全點位置的選取是以是否讓程序長時間執行的特徵爲標準選定的; 例如:方法調用,循環跳轉,異常跳轉等,只有具備這些功能的指令纔會產生安全點;數據結構
如何在垃圾收集時讓全部的線程都在最近的安全點停頓下來,有兩種方案可選,搶先式終端、主動式中斷 搶先式中斷不須要線程主動配合,在垃圾收集時,系統吧用戶線程所有中斷,若是發現用戶線程不在安全點上,那麼恢復這個線程,讓它一會再中斷,直到它跑到安全點上。 主動式中斷的思想是當垃圾收集須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一 個標誌位,各個線程執行過程時會不停地主動去輪詢這個標誌,一旦發現中斷標誌爲真時就本身在最 近的安全點上主動中斷掛起。輪詢標誌的地方和安全點是重合的,另外還要加上全部建立對象和其餘 須要在Java堆上分配內存的地方,這是爲了檢查是否即將要發生垃圾收集,避免沒有足夠內存分配新對象。多線程
另外一種中斷位置叫作安全區域,若是用戶線程處於sleep或者blocked狀態,線程沒法響應虛擬機中斷請求,就不能到安全點掛起本身,這種狀況就須要安全區域,在某一段代碼片斷中,引用關係不會發生變化,所以這這個區域中任何地方開始垃圾收集都是安全的。併發
在年輕代垃圾收集中,有些對象是直接和gcroot關聯的,而有些對象是經過老年代間接的和gcroot關聯 若是要在一次young gc中也掃描關聯了gcroot的老年代才能進行,那麼會增大回收器掃描的效率,以下圖所示
標記階段是全部追蹤式垃圾收集算法的共同特徵,若是能較少這部分的停頓時間,那麼收益將會是惠及每一個追蹤式垃圾收集器; 引入三色標記來做爲輔助,把遍歷對象圖過程當中遇到的對象,按照「是否訪問過」這個條件標記成如下三種顏色:
白色:表示對象還沒有被垃圾收集器訪問過。顯然在可達性分析剛剛開始的階段,全部的對象都是 白色的,若在分析結束的階段,仍然是白色的對象,即表明不可達。
黑色:表示對象已經被垃圾收集器訪問過,且這個對象的全部引用都已經掃描過。黑色的對象代 表已經掃描過,它是安全存活的,若是有其餘對象引用指向了黑色對象,無須從新掃描一遍。黑色對 象不可能直接(不通過灰色對象)指向某個白色對象。
灰色:表示對象已經被垃圾收集器訪問過,但這個對象上至少存在一個引用尚未被掃描過。
把掃描對象圖想象爲一股從灰色爲波紋從黑色向白色推動的過程,那若是用戶線程與收集器併發工做,收集器在對象圖上標記顏色,用戶線程在運行時修改關係,就會對對象的引用關係形成影響,使得原本要被清除的對象得以存活,這只是形成了「浮動垃圾」,而若是把原本存活的對象清除,那就會形成程序錯誤了; 所以解決併發掃描時,存活對象被清掃一般使用兩種方法;
對象消失主要有兩個操做引發
1 賦值器插入了一條或多條從黑色對象到白色對象的新引用; 2 賦值器刪除了所有從灰色對象到該白色對象的直接或間接引用。
增量更新是破壞第一個條件,當黑色對象插入新的指向白色對象的引用關係時,就將這個新 插入的引用記錄下來,等併發掃描結束以後,再將這些記錄過的引用關係中的黑色對象爲根,從新掃 描一次。這能夠簡化理解爲,黑色對象一旦新插入了指向白色對象的引用以後,它就變回灰色對象 了。
原始快照要破壞的是第二個條件,當灰色對象要刪除指向白色對象的引用關係時,就將這個要刪 除的引用記錄下來,在併發掃描結束以後,再將這些記錄過的引用關係中的灰色對象爲根,從新掃描 一次。這也能夠簡化理解爲,不管引用關係刪除與否,都會按照剛剛開始掃描那一刻的對象圖快照來 進行搜索。
以上的引用關係記錄是經過寫屏障實現的,cms使用的是增量更新,g1使用原始快照;
查看本身服務器垃圾收集器
java -XX:+PrintCommandLineFlags -version
-XX:+UseParallelGC 也是java8 默認的垃圾收集器 UseParallelGC 即 Parallel Scavenge + Parallel Old
那就先熟悉下這兩個垃圾收集器吧
Parallel Scavenge收集器也是一款新生代收集器,它一樣是基於標記-複製算法實現的收集器; 它的目標是達到一個可控制的吞吐量,所謂吞吐量就是處理器用戶運行用戶代碼的時間與處理器消耗時間的比值,與cms這種儘量縮短停頓時間的收集器不一樣,parallel scavenge在吞吐量的控制上下了更多功夫;
有同窗問我,停頓時間越小,那程序運行時間越長,那吞吐量不就越高嗎,那是否是說的是一回事, 其實不是,好比 程序運行2s 垃圾回收停頓1s,另外一種運行10s 停頓2s,那麼第一種停頓時間是1s小於第二種,但吞吐量要比第二種小的多
-XX:M axGCPauseM illis參數容許的值是一個大於0的毫秒數,收集器將盡力保證內存回收花費的 時間不超過用戶設定值。不過你們不要異想天開地認爲若是把這個參數的值設置得更小一點就能使得 系統的垃圾收集速度變得更快,垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間爲代價換取的: 系統把新生代調得小一些,收集300MB新生代確定比收集500MB快,但這也直接致使垃圾收集發生得 更頻繁,原來10秒收集一次、每次停頓100毫秒,如今變成5秒收集一次、每次停頓70毫秒。停頓時間 的確在降低,但吞吐量也降下來了。
-XX:GCTimeRat io參數的值則應當是一個大於0小於100的整數,也就是垃圾收集時間佔總時間的 比率,至關於吞吐量的倒數。譬如把此參數設置爲19,那容許的最大垃圾收集時間就佔總時間的5% (即1/(1+19)),默認值爲99,即容許最大1%(即1/(1+99))的垃圾收集時間。
除上述兩個參數以外,ParallelScavenge收集器還有一個參數-XX:+UseAdaptiveSizePolicy值得咱們關注。這是一個開關參數,當這個參數被激活以後,就不須要人工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。這種調節方式稱爲垃圾收集的自適應的調節策略(GCErgonomics)
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程併發收集,基於標記-整理算法,
咱們服務器使用以上兩種吞吐量優先的服務器看出咱們更看重服務的吞吐量,容許程序的更長時間停頓,順便說一句,若是程序的請求量更高,那麼長時間的停頓可能會在沒有自適應負載均衡系統的服務中形成高停頓,若是沒作好限流的話可能會形成整個微服務的服務雪崩;
下面介紹一個以低延遲低停頓爲主的垃圾收集器cms
大名鼎鼎的CMS(Concurrent Mark Sweep),是一種以獲取最短挺短期爲牧鞭的的收集器,大部分java應用集中在基於瀏覽器的B/S系統的服務器上,這類應用一般會較爲關注服務響應速度,但願停頓時間更短,以給用戶帶來良好的體驗,cms基於標記清除的老年代回收器,主要分爲四個步驟:
1)初始標記(CMS initial mark)
2)併發標記(CM S concurrent mark)
3)從新標記(CM S remark)
4)併發清除(CM S concurrent sweep)
其中須要停頓的是第一步初始標記和第三部從新標記,初始標記是標記gc roots能直接關聯到的對象,速度很快 併發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對 象圖的過程,這個過程耗時較長可是不須要停頓用戶線程,能夠與垃圾收集線程一塊兒併發運行; 而從新標記階段則是爲了修正併發標記期間,因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,前邊有講過cms是採用增量更新的方式處理的,這段的停頓時間稍長;最後是併發清除階段,清理刪除掉標記階段判斷的已經死亡的 對象,因爲不須要移動存活對象,因此這個階段也是能夠與用戶線程同時併發的。
面向併發設計的程序都對處理器資源比較敏 感。在併發階段,它雖然不會致使用戶線程停頓,但卻會由於佔用了一部分線程(或者說處理器的計 算能力)而致使應用程序變慢,下降總吞吐量;
CMS沒法處理浮動垃圾,有可能出現併發失敗形成一次 stop the world的full fc,由於標記清理過程當中用戶線程和垃圾回收線程併發運行,程序運行會產生新的垃圾對象,但這部分對象是出如今標記過程以後的,CMS沒法在本次收集中清理掉,就要留到下一次收集,這一部分就是浮動垃圾,因此CMS不能等到老年代滿了纔開始收集,必須預留一部分空間給程序運行使用,java5默認是68%,java6的默認數值提高到了92%,那若是預留的空間仍是沒法知足程序運行時新對象分配,那麼會出現一次併發失敗,虛擬機會凍結用戶線程,臨時啓用 serial old收集器(一個單線程的老年代垃圾收集器)來收集垃圾,這樣的停頓時間就更長了,因此參數 -XX:CMSInitiatingOccupancyFraction設置得過高將會很容易致使 大量的併發失敗產生,性能反而下降,用戶應在生產環境中根據實際應用狀況來權衡設置。
還有一個重要的關注點,CMS是一款基於標記-清除算法實現的垃圾收集器,這種算法會產生不少空間碎片,將會給大對象分配帶來麻煩,會出現老年代還有不少空間,可是沒法找到連續的足夠大空間來分配對象不得不進行一次full gc,CMS收集器提供了一個參數,+UseCMS-CompactAtFullCollection開關參數,用於在CMS收集器不得不進行FullGC時開啓內存碎片的合併整理過程,因爲這個內存整理必須移動存活對象,是沒法併發的。這樣空間碎片問題是解決了,但停頓時間又會變長,所以虛擬機設計者們還提供了另一個參數-XX:CMSFullGCsBefore-Compaction(此參數從JDK9開始廢棄),這個參數的做用是要求CMS收集器在執行過若干次(數量由參數值決定)不整理空間的FullGC以後,下一次進入FullGC前會先進行碎片整理(默認值爲0,表示每次進入FullGC時都進行碎片整理)。
G1是一款主要面向服務端應用的垃圾收集器。HotSpot開發團隊最初賦予它的指望是(在比較長 期的)將來能夠替換掉JDK 5中發佈的CMS收集器。如今這個指望目標已經實現過半了,JDK 9發佈之 日,G1宣告取代Parallel Scavenge加Parallel Old組合,成爲服務端模式下的默認垃圾收集器,而CMS則 淪落至被聲明爲不推薦使用(Deprecate)的收集器[1]。若是對JDK 9及以上版本的HotSpot虛擬機使用 參數-XX:+UseConcMarkSweep GC來開啓CM S收集器的話,用戶會收到一個警告信息,提示CM S未 來將會被廢棄
G1,能夠面向全堆的任何組成部分回收,衡量標準再也不是對象屬於哪一個分代,而是哪塊內存中存放的垃圾數量最多,回收效益最大,雖然它也是遵循分代收集理論,可是堆內存佈局與其餘收集器有很是明顯的差別,G1再也不堅持固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每一個Region能夠根據須要扮演新生代的Eden空間、Survivor空間,或者老年代空間,G1根據扮演不一樣角色的Region採用不一樣的策略去處理; Humongous區域是一種特殊的Region,專用來存儲大對象,只要超過一個Region容量的一半就斷定爲大對象。
G1會去跟蹤每一個Region裏邊垃圾回收的價值大小,價值就是回收所得到的空間大小以及回收所須要時間的經驗值,會在後臺維護一個優先級列表,根據用戶設定容許的收集停頓時間,優先處理收益最大的Region,使用記憶集來避免跨Region的掃描,G1維護一個哈希表做爲記憶集,key是別的Region其實地址,value是個集合,存儲元素是卡表的索引號,因爲Region的數量比傳統收集器的分代數量明顯要多得多,所以G1收集器要比其餘的傳統垃 圾收集器有着更高的內存佔用負擔。
初始標記(Initial M arking):僅僅只是標記一下GC Roots能直接關聯到的對象,而且修改TAM S 指針的值,讓下一階段用戶線程併發運行時,能正確地在可用的Region中分配新對象。這個階段須要 停頓線程,但耗時很短,並且是借用進行Minor GC的時候同步完成的,因此G1收集器在這個階段實際 並無額外的停頓。
併發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆 裏的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序併發執行。當對象圖掃描完成以 後,還要從新處理SAT B記錄下的在併發時有引用變更的對象。
最終標記(Final M arking):對用戶線程作另外一個短暫的暫停,用於處理併發階段結束後仍遺留 下來的最後那少許的SAT B記錄。
篩選回收(Live Data Counting and Evacuation):負責更新Region的統計數據,對各個Region的回 收價值和成本進行排序,根據用戶所指望的停頓時間來制定回收計劃,能夠自由選擇任意多個Region 構成回收集,而後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整個舊 Region的所有空間。這裏的操做涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行 完成的。
目前在小內存應用上CMS的表現大機率仍然要會優於G1,而在大內存應用上G1則大多能發揮其 優點,這個優劣勢的Java堆容量平衡點一般在6GB至8GB之間,固然,以上這些也僅是經驗之談,不 同應用須要量體裁衣地實際測試才能得出最合適的結論,隨着HotSpot的開發者對G1的不斷優化,也 會讓對比結果繼續向G1傾斜。
ZGC目標是在儘量對吞吐量影響不太大的前提下,實如今任意堆內存大小下均可以把垃圾收集的停頓時間限制在十毫秒之內的低延遲; ZGC收集器是一款基於Region內存佈局的,(暫時) 不設分代的,使用了讀屏障、染色指針和內存多重映射等技術來實現可併發的標記-整理算法的,以低 延遲爲首要目標的一款垃圾收集器; 染色指針是一種直接將少許額外的信息存儲在指針上的技術,染色指針可使得一旦某個Region的存活對象被移走以後,這個Region當即就可以被釋放和重用掉,而沒必要等待整個堆中全部指向該Region的引用都被修正後才能清理;
ZGC使用了多重映射(Multi-Mapping)將多個不一樣的虛擬內存地址映射到同一 個物理內存地址上,這是一種多對一映射,意味着ZGC在虛擬內存中看到的地址空間要比實際的堆內 存容量來得更大。把染色指針中的標誌位看做是地址的分段符,那隻要將這些不一樣的地址段都映射到 同一個物理內存空間,通過多重映射轉換後,就可使用染色指針正常進行尋址了
併發標記(ConcurrentMark):與G一、Shenandoah同樣,併發標記是遍歷對象圖作可達性分析的階段,先後也要通過相似於G一、Shenandoah的初始標記、最終標記(儘管ZGC中的名字不叫這些)的短暫停頓,並且這些停頓階段所作的事情在目標上也是相相似的。與G一、Shenandoah不一樣的是,ZGC的標記是在指針上而不是在對象上進行的,標記階段會更新染色指針中的Marked0、Marked1標誌位。
併發預備重分配(ConcurrentPrepareforRelocate):這個階段須要根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(RelocationSet)。重分配集與G1收集器的回收集(CollectionSet)仍是有區別的,ZGC劃分Region的目的並不是爲了像G1那樣作收益優先的增量回收。相反,ZGC每次回收都會掃描全部的Region,用範圍更大的掃描成本換取省去G1中記憶集的維護成本。所以,ZGC的重分配集只是決定了裏面的存活對象會被從新複製到其餘的Region中,裏面的Region會被釋放,而並不能說回收行爲就只是針對這個集合裏面的Region進行,由於標記過程是針對全堆的。此外,在JDK12的ZGC中開始支持的類卸載以及弱引用的處理,也是在這個階段中完成的。
併發重分配(ConcurrentRelocate):重分配是ZGC執行過程當中的核心階段,這個過程要把重分配集中的存活對象複製到新的Region上,併爲重分配集中的每一個Region維護一個轉發表(ForwardTable),記錄從舊對象到新對象的轉向關係。得益於染色指針的支持,ZGC收集器能僅從引用上就明確得知一個對象是否處於重分配集之中,若是用戶線程此時併發訪問了位於重分配集中的對象,此次訪問將會被預置的內存屏障所截獲,而後當即根據Region上的轉發表記錄將訪問轉發到新複製的對象上,並同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行爲稱爲指針的「自愈」(Self-Healing)能力。這樣作的好處是隻有第一次訪問舊對象會陷入轉發,也就是隻慢一次,對比Shenandoah的Brooks轉發指針,那是每次對象訪問都必須付出的固定開銷,簡單地說就是每次都慢,所以ZGC對用戶程序的運行時負載要比Shenandoah來得更低一些。還有另一個直接的好處是因爲染色指針的存在,一旦重分配集中某個Region的存活對象都複製完畢後,這個Region就能夠當即釋放用於新對象的分配(可是轉發表還得留着不能釋放掉),哪怕堆中還有不少指向這個對象的未更新指針也沒有關係,這些舊指針一旦被使用,它們都是能夠自愈的。
併發重映射(ConcurrentRemap):重映射所作的就是修正整個堆中指向重分配集中舊對象的全部引用,這一點從目標角度看是與Shenandoah併發引用更新階段同樣的,可是ZGC的併發重映射並非一個必需要「迫切」去完成的任務,由於前面說過,即便是舊引用,它也是能夠自愈的,最多隻是第一次使用時多一次轉發和修正操做。重映射清理這些舊引用的主要目的是爲了避免變慢(還有清理結束後能夠釋放轉發表這樣的附帶收益),因此說這並非很「迫切」。所以,ZGC很巧妙地把併發重映射階段要作的工做,合併到了下一次垃圾收集循環中的併發標記階段裏去完成,反正它們都是要遍歷所 有對象的,這樣合併就節省了一次遍歷對象圖[9]的開銷。一旦全部指針都被修正以後,原來記錄新舊對象關係的轉發表就能夠釋放掉了。
-XX:+PrintGC 輸出GC日誌
-XX:+PrintGCDetails 輸出GC的詳細日誌
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進行GC的先後打印出堆的信息
-Xloggc:../logs/gc.log 日誌文件的輸出路徑
嘗試打出的gc日誌
[GC (System.gc()) [PSYoungGen: 5022K->1152K(38400K)] 5022K->1160K(125952K), 0.0019083 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1152K->0K(38400K)] [ParOldGen: 8K->1034K(87552K)] 1160K->1034K(125952K), [Metaspace: 3139K->3139K(1056768K)], 0.0045528 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap PSYoungGen total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000) eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000) from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000) to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000) ParOldGen total 87552K, used 1034K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000) object space 87552K, 1% used [0x0000000740000000,0x0000000740102bb0,0x0000000745580000) Metaspace used 3147K, capacity 4564K, committed 4864K, reserved 1056768K class space used 340K, capacity 388K, committed 512K, reserved 1048576K
![]()
個人服務器20秒左右一次young gc,沒有full gc,說明運行情況良好 若是頻繁full gc,會引發頻繁停頓,如下狀況會致使fullgc
一、System.gc()方法的調用 二、老年代空間不足 三、永生區空間不足 四、CMS GC時出現concurrent mode failure 五、堆中分配很大的對象
jps查處進程id爲5280
jmap -dump:format=b,file=temp.dump 5280 把文件dump出來,再經過jvisualvm分析對象的引用鏈的方式來定位具體頻繁建立對象的地方。
今天學習了追蹤式垃圾收集的過程,包括根結點枚舉,併發可達性分析,三色標記,跨代(Region)引用的卡表,寫屏障,以後又熟悉了幾個java中經典的垃圾收集器,分析了各個經典垃圾收集器的優缺點和使用場景,今天,大多數互聯網公司都在用java8,等下一個java11的時代來臨,咱們能夠大範圍的使用java11的zgc,相信這一天就快到來了,最後咱們學習了查看gc日誌的方式,以及簡單講解了如何排查頻繁full gc,那今天就到這了,喜歡的朋友一鍵三連把,手動狗頭。
預先善其事必先利其器,下期聊下jmap相似的java自帶的排查問題工具