JVM調(diào)優(yōu)之垃圾定位、垃圾回收算法、垃圾處理器對比
點擊上方藍色字體,選擇“標星公眾號”
優(yōu)質(zhì)文章,第一時間送達
? 作者?|? 等不到的口琴
來源 |? urlify.cn/V3URZj
談垃圾回收器之前,要先講講垃圾回收算法,以及JVM對垃圾的認定策略,JVM垃圾回收器是垃圾回收算法的具體實現(xiàn),了解了前面的前置知識,有利于對垃圾回收器的理解。
什么是垃圾?
垃圾,主要是指堆上的對象,那么如何確定這些對象是可以被回收的呢?
大概思路就是,如果一個對象永遠不可能被訪問到,那么就是垃圾,可以被回收了如何確定對象永遠不會被使用呢?
引用計數(shù)法
在對象中添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器值就加一;當引用失效時,計數(shù)器值就減一;任何時刻計數(shù)器為零的對象就是不可能再被使用的。但是,在Java領域,至少主流的Java虛擬機里面都沒有選用引用計數(shù)算法來管理內(nèi)存,主要原因是,這個看似簡單的算法有很多例外情況要考慮,必須要配合大量額外處理才能保證正確地工作,譬如單純的引用計數(shù)就很難解決對象之間相互循環(huán)引用的問題。

如圖,每一個對象的引用都是1,構成了循環(huán)引用,但是并不能被其他對象訪問,這兩個對象再無任何引用,引用計數(shù)算法也就無法回收它們。
代碼驗證:
package?com.courage;
public?class?ReferenceCountingGC?{
????public?Object?instance?=?null;
????private?static?final?int?_1MB?=?1024?*?1024;
????/**
?????*?這個成員屬性的唯一意義就是占點內(nèi)存,以便能在GC日志中看清楚是否有回收過
?????*/
????private?byte[]?bigSize?=?new?byte[5*?_1MB];
????public?static?void?testGC()?{
????????//5?M
????????ReferenceCountingGC?objA?=?new?ReferenceCountingGC();
????????//5?M
????????ReferenceCountingGC?objB?=?new?ReferenceCountingGC();
????????objA.instance?=?objB;
????????objB.instance?=?objA;
????????objA?=?null;
????????objB?=?null;
//?假設在這行發(fā)生GC,objA和objB是否能被回收?
????????System.gc();
????}
????public?static?void?main(String[]?args)?{
????????testGC();
????}
}
執(zhí)行結果:
[0.004s][warning][gc]?-XX:+PrintGCDetails?is?deprecated.?Will?use?-Xlog:gc*?instead.
[0.012s][info???][gc,heap]?Heap?region?size:?1M
[0.015s][info???][gc?????]?Using?G1
[0.015s][info???][gc,heap,coops]?Heap?address:?0x0000000701000000,?size:?4080?MB,?Compressed?Oops?mode:?Zero?based,?Oop?shift?amount:?3
......
[0.119s][info???][gc,metaspace???]?GC(0)?Metaspace:?805K->805K(1056768K)
[0.119s][info???][gc?????????????]?GC(0)?Pause?Full?(System.gc())?14M->0M(8M)?2.886ms
[0.119s][info???][gc,cpu?????????]?GC(0)?User=0.03s?Sys=0.00s?Real=0.00s
[0.120s][info???][gc,heap,exit???]?Heap
......
為了篇幅,我將部分打印內(nèi)容省略了,可見System.gc()后內(nèi)存占用由14M->0M,將對象這10M釋放了。也就是JVM里面并沒使用引用計數(shù)法來標記垃圾。
根可達算法
這個算法的基本思路就是通過一系列稱為“GC Roots”的根對象作為起始節(jié)點集,從這些節(jié)點開始,根據(jù)引用關系向下搜索,搜索過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。

在Java技術體系里面,固定可作為GC Roots的對象包括以下幾種:
在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調(diào)用的方法堆棧中使用到的
參數(shù)、局部變量、臨時變量等在方法區(qū)中類靜態(tài)屬性引用的對象,譬如Java類的引用類型靜態(tài)變量。在方法區(qū)中常量引用的對象,譬如字符串常量池(String Table)里的引用。
在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
Java虛擬機內(nèi)部的引用,如基本數(shù)據(jù)類型對應的Class對象,一些常駐的異常對象(比如
NullPointExcepiton、OutOfMemoryError)等,還有系統(tǒng)類加載器。所有被同步鎖(synchronized關鍵字)持有的對象。
反映Java虛擬機內(nèi)部情況的JMXBean、JVMTI中注冊的回調(diào)、本地代碼緩存等。
垃圾回收算法
本文介紹了常見的三種垃圾回收算法(mark-sweep,mark-compact,mark-copy),是java虛擬機各種垃圾收集器的算法基礎。
垃圾回收算法思想
當前商業(yè)虛擬機的垃圾收集器,大多數(shù)都遵循了“分代收集”(Generational Collection)的理論進行設計,分代收集名為理論,實質(zhì)是一套符合大多數(shù)程序運行實際情況的經(jīng)驗法則,它建立在兩個分代假說之上:
1)弱分代假說(Weak Generational Hypothesis):絕大多數(shù)對象都是朝生夕滅的。
2)強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。
這兩個分代假說共同奠定了多款常用的垃圾收集器的一致的設計原則:收集器應該將Java堆劃分出不同的區(qū)域,然后將回收對象依據(jù)其年齡(年齡即對象熬過垃圾收集過程的次數(shù))分配到不同的區(qū)域之中存儲顯而易見,如果一個區(qū)域中大多數(shù)對象都是朝生夕滅,難以熬過垃圾收集過程的話,那么把它們集中放在一起,每次回收時只關注如何保留少量存活而不是去標記那些大量將要被回收的對象,就能以較低代價回收到大量的空間;
如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛擬機便可以使用較低的頻率來回收這個區(qū)域,這就同時兼顧了垃圾收集的時間開銷和內(nèi)存的空間有效利用。
標記-清除算法 Mark-Sweep
算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后,統(tǒng)一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統(tǒng)一回收所有未被標記的對象。

它的主要缺點有兩個:
第一個是執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執(zhí)行效率都隨對象數(shù)量增長而降低;
第二個是內(nèi)存空間的碎片化問題,標記、清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導致當以后在程序運行過程中需要分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。
標記-復制 Mark-Copy
標記-復制算法常被簡稱為復制算法它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。

如果內(nèi)存中多數(shù)對象都是存活的,這種算法將會產(chǎn)生大量的內(nèi)存間復制的開銷,但對于多數(shù)對象都是可回收的情況,算法需要復制的就是占少數(shù)的存活對象,而且每次都是針對整個半?yún)^(qū)進行內(nèi)存回收,分配內(nèi)存時也就不用考慮有空間碎片的復雜情況,只要移動堆頂指針,按順序分配即可。這樣實現(xiàn)簡單,運行高效,不過其缺陷也顯而易見,這種復制回收算法的代價是將可用內(nèi)存縮小為了原來的一半,空間浪費未免太多了。
標記-壓縮 Mark-Compact
標記-復制算法在對象存活率較高時就要進行較多的復制操作,效率將會降低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
標記-壓縮算法其中的標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內(nèi)存空間一端移動,然后直接清理掉邊界以外的內(nèi)存:

標記-清除算法與標記-整理算法的本質(zhì)差異在于前者是一種非移動式的回收算法,而后者是移動式的。是否移動回收后的存活對象是一項優(yōu)缺點并存的風險決策:如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區(qū)域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序(STW問題)才能進行 。
垃圾處理器
基于上面的三種垃圾回收算法,衍生出7種垃圾回收器:
Serial收集器
這個收集器是一個單線程工作的收集器,但它的“單線程”的意義并不僅僅是說明它只會使用一個處理器或一條收集線程去完成垃圾收集工作,更重要的是強調(diào)在它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束。

迄今為止,它依然是HotSpot虛擬機運行在客戶端模式下的默認新生代收集器,有著優(yōu)于其他收集器的地方,那就是簡單而高效(與其他收集器的單線程相比),對于內(nèi)存資源受限的環(huán)境,它是所有收集器里額外內(nèi)存消耗(Memory Footprint) [1] 最小的;對于單核處理器或處理器核心數(shù)較少的環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。Serial收集器對于運行在客戶端模式下的虛擬機來說是一個很好的選擇。
ParNew收集器
ParNew收集器實質(zhì)上是Serial收集器的多線程并行版本,除了同時使用多條線程進行垃圾收集之
外,其余的行為包括Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:
PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規(guī)
則、回收策略等都與Serial收集器完全一致,在實現(xiàn)上這兩種收集器也共用了相當多的代碼。

ParNew收集器除了支持多線程并行收集之外,其他與Serial收集器相比并沒有太多創(chuàng)新之處,但它
卻是不少運行在服務端模式下的HotSpot虛擬機,尤其是JDK 7之前的遺留系統(tǒng)中首選的新生代收集
器,其中有一個與功能、性能無關但其實很重要的原因是:除了Serial收集器外,目前只有它能與CMS
收集器配合工作,另一方面CMS的出現(xiàn)鞏固了ParNew的地位
ParNew收集器在單核心處理器的環(huán)境中絕對不會有比Serial收集器更好的效果,甚至由于存在線程
交互的開銷,該收集器在通過超線程(Hyper-Threading)技術實現(xiàn)的偽雙核處理器環(huán)境中都不能百分之百保證超越Serial收集器。當然,隨著可以被使用的處理器核心數(shù)量的增加,ParNew對于垃圾收集時
系統(tǒng)資源的高效利用還是很有好處的。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同樣是基于標記-復制算法實現(xiàn)的收集器,也是能夠并行收集的多線程收集器……Parallel Scavenge的諸多特性從表面上看和ParNew非常相似,那它有什么特別之處呢?
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是處理器用于運行用戶代碼的時間與處理器總消耗時間的比值,即:
吞吐量=運行用戶代碼時間運行用戶代碼時間+運行垃圾收集器的時間
如果虛擬機完成某個任務,用戶代碼加上垃圾收集總共耗費了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。停頓時間越短就越適合需要與用戶交互或需要保證服務響應質(zhì)量的程序,良好的響應速度能提升用戶體驗;而高吞吐量則可以最高效率地利用處理器資源,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的分析任務。
由于與吞吐量關系密切,Parallel Scavenge收集器也經(jīng)常被稱作“吞吐量優(yōu)先收集器”。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法。這個收集器的主要意義也是供客戶端模式下的HotSpot虛擬機使用。如果在服務端模式下,它也可能有兩種用途:一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用,另外一種就是作為CMS收集器發(fā)生失敗時的后備預案,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用。

Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程并發(fā)收集,基于標記-整理算法實現(xiàn)。這個收集器是直到JDK 6時才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于相當尷尬的狀態(tài),原因是如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PSMarkSweep)收集器以外別無選擇,其他表現(xiàn)良好的老年代收集器,如CMS無法與它配合工作。
由于老年代Serial Old收集器在服務端應用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整體上獲得吞吐量最大化的效果。
同樣,由于單線程的老年代收集中無法充分利用服務器多處理器的并行處理能力,在老年代內(nèi)存空間很大而且硬件規(guī)格比較高級的運行環(huán)境中,這種組合的總吞吐量甚至不一定比ParNew加CMS的組合來得優(yōu)秀。直到Parallel Old收集器出現(xiàn)后,“吞吐量優(yōu)先”收集器終于有了比較名副其實的搭配組合,在注重吞吐量或者處理器資源較為稀缺的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器這個組合。

CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯(lián)網(wǎng)網(wǎng)站或者基于瀏覽器的B/S系統(tǒng)的服務端上,這類應用通常都會較為關注服務的響應速度,希望系統(tǒng)停頓時間盡可能短,以給用戶帶來良好的交互體驗。CMS收集器就非常符合這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于標記-清除算法實現(xiàn)的,它的運作過程相對于前面幾種收集器來說要更復雜一些,整個過程分為四個步驟,包括:
1)初始標記(CMS initial mark)
2)并發(fā)標記(CMS concurrent mark)
3)重新標記(CMS remark)
4)并發(fā)清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GCRoots能直接關聯(lián)到的對象,速度很快;并發(fā)標記階段就是從GC Roots的直接關聯(lián)對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發(fā)運行;而重新標記階段則是為了修正并發(fā)標記期間,因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比并發(fā)標記階段的時間短;最后是并發(fā)清除階段,清理刪除掉標記階段判斷的已經(jīng)死亡的對象,由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發(fā)的。由于在整個過程中耗時最長的并發(fā)標記和并發(fā)清除階段中,垃圾收集器線程都可以與用戶線程一起工作,所以從總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。
優(yōu)點:并發(fā)收集、低停頓
缺點:1.對處理器資源非常敏感
2.無法處理“浮動垃圾”(Floating Garbage)
3.空間碎片
Garbage First收集器
Garbage First(簡稱G1)收集器是垃圾收集器技術發(fā)展歷史上的里程碑式的成果,它開創(chuàng)了收集器面向局部收集的設計思路和基于Region的內(nèi)存布局形式。G1是一款主要面向服務端應用的垃圾收集器。
在G1收集器出現(xiàn)之前的所有其他收集器,包括CMS在內(nèi),垃圾收集的目標范圍要么是整個新生代(Minor GC),要么就是整個老年代(Major GC),再要么就是整個Java堆(Full GC)。而G1跳出了這個樊籠,它可以面向堆內(nèi)存任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬于哪個分代,而是哪塊內(nèi)存中存放的垃圾數(shù)量最多,回收收益最大,這就是G1收集器的Mixed GC模式。G1開創(chuàng)的基于Region的堆內(nèi)存布局是它能夠?qū)崿F(xiàn)這個目標的關鍵。雖然G1也仍是遵循分代收集理論設計的,但其堆內(nèi)存的布局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把連續(xù)的Java堆劃分為多個大小相等的獨立區(qū)域(Region),每一個Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠?qū)Π缪莶煌巧腞egion采用不同的策略去處理,這樣無論是新創(chuàng)建的對象還是已經(jīng)存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。
Region中還有一類特殊的Humongous區(qū)域,專門用來存儲大對象。G1認為只要大小超過了一個Region容量一半的對象即可判定為大對象。每個Region的大小可以通過參數(shù)-XX:G1HeapRegionSize設定,取值范圍為1MB~32MB,且應為2的N次冪。而對于那些超過了整個Region容量的超級大對象,將會被存放在N個連續(xù)的Humongous Region之中,G1的大多數(shù)行為都把Humongous Region作為老年代的一部分來進行看待,如圖3-12所示。
雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它們都是一系列區(qū)域(不需要連續(xù))的動態(tài)集合。G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,即每次收集到的內(nèi)存空間都是Region大小的整數(shù)倍,這樣可以有計劃地避免在整個Java堆中進行全區(qū)域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個Region里面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經(jīng)驗值,然后在后臺維護一個優(yōu)先級列表,每次根據(jù)用戶設定允許的收集停頓時間(使用參數(shù)-XX:MaxGCPauseMillis指定,默認值是200毫秒),優(yōu)先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。這種使用Region劃分內(nèi)存空間,以及具有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限的時間內(nèi)獲取盡可能高的收集效率。
垃圾處理器總結
目前是新生代老年代垃圾回收器組合方式:

鋒哥最新SpringCloud分布式電商秒殺課程發(fā)布
??????
??長按上方微信二維碼?2 秒
感謝點贊支持下哈?
