GC知識點(diǎn)回顧:
垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供的一種用于在空閑時間不定時回收無任何對象引用的對象占據(jù)的內(nèi)存空間的一種機(jī)制。
注意:垃圾回收回收的是無任何引用的對象占據(jù)的內(nèi)存空間而不是對象本身窿克。換言之,垃圾回收只會負(fù)責(zé)釋放那些對象占有的內(nèi)存徘溢。對象是個抽象的詞景东,包括引用和其占據(jù)的內(nèi)存空間砂轻。當(dāng)對象沒有任何引用時其占據(jù)的內(nèi)存空間隨即被收回備用,此時對象也就被銷毀斤吐。但不能說是回收對象搔涝,可以理解為一種文字游戲。
引用 :?如果Reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址和措,就稱這塊內(nèi)存代表著一個引用庄呈。引用又分為強(qiáng)引用,軟引用派阱,弱引用诬留,虛引用。
?① : 強(qiáng)引用(Strong Reference):如“Object obj = new Object()”贫母,這類引用是Java程序中最普遍的文兑。只要強(qiáng)引用還存在,垃圾收集器就永遠(yuǎn)不會回收掉被引用的對象颁独。
?② : 軟引用(Soft Reference):它用來描述一些可能還有用彩届,但并非必須的對象。在系統(tǒng)內(nèi)存不夠用時誓酒,這類引用關(guān)聯(lián)的對象將被垃圾收集器回收。JDK1.2之后提供了SoftReference類來實(shí)現(xiàn)軟引用贮聂。
?③ : 弱引用(Weak Reference):它也是用來描述非須對象的靠柑,但它的強(qiáng)度比軟引用更弱些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前吓懈。當(dāng)垃圾收集器工作時歼冰,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象耻警。在JDK1.2之后隔嫡,提供了WeakReference類來實(shí)現(xiàn)弱引用。
?④ : 虛引用(Phantom Reference):最弱的一種引用關(guān)系甘穿,完全不會對其生存時間構(gòu)成影響腮恩,也無法通過虛引用來取得一個對象實(shí)例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個對象被收集器回收時收到一個系統(tǒng)通知温兼。JDK1.2之后提供了PhantomReference類來實(shí)現(xiàn)虛引用秸滴。
判斷對象是否是垃圾的算法
Java語言規(guī)范沒有明確地說明JVM使用哪種垃圾回收算法,但是任何一種垃圾回收算法一般要做2件基本的事情:
① : 找到所有存活對象募判;
② : 回收被無用對象占用的內(nèi)存空間荡含,使該空間可被程序再次使用咒唆。
通常有以下幾種算法來判斷對象是否已死:
引用計數(shù)算法(Reference Counting Collector)
堆中每個對象(不是引用)都有一個引用計數(shù)器。當(dāng)一個對象被創(chuàng)建并初始化賦值后释液,該變量計數(shù)設(shè)置為1全释。每當(dāng)有一個地方引用它時,計數(shù)器值就加1(a = b误债, b被引用恨溜,則b引用的對象計數(shù)+1)。當(dāng)引用失效時(一個對象的某個引用超過了生命周期(出作用域后)或者被設(shè)置為一個新值時)找前,計數(shù)器值就減1糟袁。任何引用計數(shù)為0的對象可以被當(dāng)作垃圾收集。當(dāng)一個對象被垃圾收集時躺盛,它引用的任何對象計數(shù)減1项戴。
優(yōu)點(diǎn):引用計數(shù)收集器執(zhí)行簡單,判定效率高槽惫,交織在程序運(yùn)行中周叮。對程序不被長時間打斷的實(shí)時環(huán)境比較有利(OC的內(nèi)存管理使用該算法)。
缺點(diǎn): 難以檢測出對象之間的循環(huán)引用界斜。同時仿耽,引用計數(shù)器增加了程序執(zhí)行的開銷。所以Java語言并沒有選擇這種算法進(jìn)行垃圾回收各薇。
早期的JVM使用引用計數(shù)项贺,現(xiàn)在大多數(shù)JVM采用對象引用遍歷(根搜索算法)。
GC算法 :?
① :? 標(biāo)記-清除算法:
標(biāo)記-清除算法是現(xiàn)代垃圾回收算法的思想基礎(chǔ)峭判。標(biāo)記-清除算法將垃圾回收分為兩個階段:標(biāo)記階段和清除階段开缎。一種可行的實(shí)現(xiàn)是,在標(biāo)記階段林螃,首先通過根節(jié)點(diǎn)奕删,標(biāo)記所有從根節(jié)點(diǎn)開始的可達(dá)對象。因此疗认,未被標(biāo)記的對象就是未被引用的垃圾對象完残;然后,在清除階段横漏,清除所有未被標(biāo)記的對象谨设。它的做法是當(dāng)堆中的有效內(nèi)存空間(available memory)被耗盡的時候,就會停止整個程序(也被成為stop the world)绊茧,然后進(jìn)行兩項工作铝宵,第一項則是標(biāo)記,第二項則是清除。
標(biāo)記 : 標(biāo)記的過程其實(shí)就是鹏秋,遍歷所有的GC Roots尊蚁,然后將所有GC Roots可達(dá)的對象標(biāo)記為存活的對象。清除:清除的過程將遍歷堆中所有的對象侣夷,將沒有標(biāo)記的對象全部清除掉横朋。也就是說,就是當(dāng)程序運(yùn)行期間百拓,若可以使用的內(nèi)存被耗盡的時候琴锭,GC線程就會被觸發(fā)并將程序暫停,隨后將依舊存活的對象標(biāo)記一遍衙传,最終再將堆中所有沒被標(biāo)記的對象全部清除掉决帖,接下來便讓程序恢復(fù)運(yùn)行。
標(biāo)記-清除算法的缺點(diǎn)
① : 首先蓖捶,它的缺點(diǎn)就是效率比較低(遞歸與全堆對象遍歷)地回,導(dǎo)致stop the world的時間比較長,尤其對于交互式的應(yīng)用程序來說簡直是無法接受俊鱼。試想一下刻像,如果你玩一個網(wǎng)站,這個網(wǎng)站一個小時就掛五分鐘并闲,你還玩嗎细睡?
② : 第二點(diǎn)主要的缺點(diǎn),則是這種方式清理出來的空閑內(nèi)存是不連續(xù)的帝火,這點(diǎn)不難理解溜徙,我們的死亡對象都是隨即的出現(xiàn)在內(nèi)存的各個角落的,現(xiàn)在把它們清除之后购公,內(nèi)存的布局自然會亂七八糟萌京。而為了應(yīng)付這一點(diǎn),JVM就不得不維持一個內(nèi)存的空閑列表宏浩,這又是一種開銷。而且在分配數(shù)組對象的時候靠瞎,尋找連續(xù)的內(nèi)存空間會不太好找比庄。
② : 復(fù)制算法:(新生代的GC):
將原有的內(nèi)存空間分為兩塊,每次只使用其中一塊乏盐,在垃圾回收時佳窑,將正在使用的內(nèi)存中的存活對象復(fù)制到未使用的內(nèi)存塊中,之后父能,清除正在使用的內(nèi)存塊中的所有對象神凑,交換兩個內(nèi)存的角色,完成垃圾回收。
. 與標(biāo)記-清除算法相比溉委,復(fù)制算法是一種相對高效的回收方法. 不適用于存活對象較多的場合鹃唯,如老年代(復(fù)制算法適合做新生代的GC)
復(fù)制算法的最大的問題是:空間的浪費(fèi)
復(fù)制算法使得每次都只對整個半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況瓣喊,只要移動堆頂指針坡慌,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡單藻三,運(yùn)行高效洪橘。只是這種算法的代價是將內(nèi)存縮小為原來的一半,這個太要命了棵帽。
所以從以上描述不難看出熄求,復(fù)制算法要想使用,最起碼對象的存活率要非常低才行逗概,而且最重要的是弟晚,我們必須要克服50%內(nèi)存的浪費(fèi)。
現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代仗谆,新生代中的對象98%都是“朝生夕死”的指巡,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊比較大的Eden空間和兩塊較小的Survivor空間隶垮,每次使用Eden和其中一塊Survivor藻雪。當(dāng)回收時,將Eden和Survivor中還存活著的對象一次性地復(fù)制到另外一塊Survivor空間上狸吞,最后清理掉Eden和剛才用過的Survivor空間勉耀。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1:1,也就是說蹋偏,每次新生代中可用內(nèi)存空間為整個新生代容量的90%(80%+10%)便斥,只有10%的空間會被浪費(fèi)。
當(dāng)然威始,98%的對象可回收只是一般場景下的數(shù)據(jù)枢纠,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當(dāng)Survivor空間不夠用時黎棠,需要依賴于老年代進(jìn)行分配擔(dān)保晋渺,所以大對象直接進(jìn)入老年代。
③ : 標(biāo)記-整理算法:(老年代的GC):
? ?如果在對象存活率較高時就要進(jìn)行較多的復(fù)制操作脓斩,效率將會變低木西。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間随静,就需要有額外的空間進(jìn)行分配擔(dān)保八千,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選中這種算法。
標(biāo)記-壓縮算法適合用于存活對象較多的場合恋捆,如老年代照皆。它在標(biāo)記-清除算法的基礎(chǔ)上做了一些優(yōu)化。和標(biāo)記-清除算法一樣鸠信,標(biāo)記-壓縮算法也首先需要從根節(jié)點(diǎn)開始纵寝,對所有可達(dá)對象做一次標(biāo)記;但之后星立,它并不簡單的清理未標(biāo)記的對象爽茴,而是將所有的存活對象壓縮到內(nèi)存的一端;之后绰垂,清理邊界外所有的空間室奏。
標(biāo)記:它的第一個階段與標(biāo)記/清除算法是一模一樣的,均是遍歷GC Roots劲装,然后將存活的對象標(biāo)記胧沫。整理:移動所有存活的對象,且按照內(nèi)存地址次序依次排列占业,然后將末端內(nèi)存地址以后的內(nèi)存全部回收绒怨。因此,第二階段才稱為整理階段谦疾。上圖中可以看到南蹂,標(biāo)記的存活對象將會被整理,按照內(nèi)存地址依次排列念恍,而未被標(biāo)記的內(nèi)存會被清理掉六剥。如此一來,當(dāng)我們需要給新對象分配內(nèi)存時峰伙,JVM只需要持有一個內(nèi)存的起始地址即可疗疟,這比維護(hù)一個空閑列表顯然少了許多開銷。
標(biāo)記/整理算法不僅可以彌補(bǔ)標(biāo)記/清除算法當(dāng)中瞳氓,內(nèi)存區(qū)域分散的缺點(diǎn)策彤,也消除了復(fù)制算法當(dāng)中,內(nèi)存減半的高額代價匣摘。
但是锅锨,標(biāo)記/整理算法唯一的缺點(diǎn)就是效率也不高。不僅要標(biāo)記所有存活對象恋沃,還要整理所有存活對象的引用地址。從效率上來說必指,標(biāo)記/整理算法要低于復(fù)制算法囊咏。
標(biāo)記-清除算法、復(fù)制算法、標(biāo)記整理算法的總結(jié):
三個算法都基于根搜索算法去判斷一個對象是否應(yīng)該被回收梅割,而支撐根搜索算法可以正常工作的理論依據(jù)霜第,就是語法中變量作用域的相關(guān)內(nèi)容。因此户辞,要想防止內(nèi)存泄露泌类,最根本的辦法就是掌握好變量作用域,而不應(yīng)該使用C/C++式內(nèi)存管理方式底燎。
在GC線程開啟時刃榨,或者說GC過程開始時,它們都要暫停應(yīng)用程序(stop the world)双仍。
它們的區(qū)別如下:(>表示前者要優(yōu)于后者枢希,=表示兩者效果一樣)
① : 效率:復(fù)制算法>標(biāo)記/整理算法>標(biāo)記/清除算法(此處的效率只是簡單的對比時間復(fù)雜度,實(shí)際情況不一定如此)朱沃。
② : 內(nèi)存整齊度:復(fù)制算法=標(biāo)記/整理算法>標(biāo)記/清除算法苞轿。
③ : 內(nèi)存利用率:標(biāo)記/整理算法=標(biāo)記/清除算法>復(fù)制算法。
注1:可以看到標(biāo)記/清除算法是比較落后的算法了逗物,但是后兩種算法卻是在此基礎(chǔ)上建立的搬卒。
注2:時間與空間不可兼得。
④ :? GC 日志 :?
要查看GC日志翎卓,需要設(shè)置一下jvm的參數(shù)契邀。關(guān)于輸出GC日志的參數(shù)有以下幾種:
-XX:+PrintGC 輸出GC日志
-XX:+PrintGCDetails 輸出GC的詳細(xì)日志
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準(zhǔn)時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進(jìn)行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的輸出路徑
通常GC的日志大概長這樣:
[GC (System.gc()) [PSYoungGen: 3686K->664K(38400K)] 3686K->672K(125952K), 0.0016607 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 664K->0K(38400K)] [ParOldGen: 8K->537K(87552K)] 672K->537K(125952K), [Metaspace: 2754K->2754K(1056768K)], 0.0059024 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen ? ? ?total 38400K, used 333K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000)
?eden space 33280K, 1% used [0x00000000d5c00000,0x00000000d5c534a8,0x00000000d7c80000)
?from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000)
?to ? space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000)
ParOldGen ? ? ? total 87552K, used 537K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000)
?object space 87552K, 0% used [0x0000000081400000,0x00000000814864a0,0x0000000086980000)
Metaspace ? ? ? used 2761K, capacity 4486K, committed 4864K, reserved 1056768K
?class space ? ?used 299K, capacity 386K, committed 512K, reserved 1048576K
GC日志開頭的”[GC”和”[Full GC”說明了這次垃圾收集的停頓類型莲祸,如果有”Full”蹂安,說明這次GC發(fā)生了”Stop-The-World”。因為是調(diào)用了System.gc()方法觸發(fā)的收集锐帜,所以會顯示”[Full GC (System.gc())”田盈,不然是沒有后面的(System.gc())的。
“[PSYoungGen”和”[ParOldGen”是指GC發(fā)生的區(qū)域缴阎,分別代表使用Parallel Scavenge垃圾收集器的新生代和使用Parallel old垃圾收集器的老生代允瞧。為什么是這兩個垃圾收集器組合呢?因為我的jvm開啟的模式是Server蛮拔,而Server模式的默認(rèn)垃圾收集器組合便是這個述暂,在命令行輸入java -version就可以看到自己的jvm默認(rèn)開啟模式。還有一種是client模式建炫,默認(rèn)組合是Serial收集器和Serial Old收集器組合畦韭。
在方括號中”PSYoungGen:”后面的”3686K->664K(38400K)”代表的是”GC前該內(nèi)存區(qū)域已使用的容量->GC后該內(nèi)存區(qū)域已使用的容量(該內(nèi)存區(qū)域總?cè)萘?”
在方括號之外的”3686K->672K(125952K)”代表的是”GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆總?cè)萘?”
再往后的”0.0016607 sec”代表該內(nèi)存區(qū)域GC所占用的時間,單位是秒肛跌。
再后面的”[Times: user=0.00 sys=0.00, real=0.00 secs]”艺配,user代表進(jìn)程在用戶態(tài)消耗的CPU時間察郁,sys代表代表進(jìn)程在內(nèi)核態(tài)消耗的CPU時間、real代表程序從開始到結(jié)束所用的時鐘時間转唉。這個時間包括其他進(jìn)程使用的時間片和進(jìn)程阻塞的時間(比如等待 I/O 完成)皮钠。
至于后面的”eden”代表的是Eden空間,還有”from”和”to”代表的是Survivor空間赠法。
線上GC實(shí)際案例分析 :?
此次線上調(diào)優(yōu)的系統(tǒng)是一個計數(shù)器服務(wù)麦轰,暫且稱其為F吧。系統(tǒng)F是一個具有滑動時間窗口的計數(shù)服務(wù)砖织,每天有幾十億次的訪問量款侵,系統(tǒng)內(nèi)部主要是小對象比較多,當(dāng)時每臺機(jī)器容量分配的是 8C16G镶苞,所以當(dāng)時GC的調(diào)整參數(shù)設(shè)置成了 -xmx 12g -xms 12g -xmn 4g喳坠,咋一看這樣設(shè)置沒啥毛病,也是jvm建議設(shè)置的參數(shù)(年輕代占總堆的1/3)茂蚓。
高峰期系統(tǒng)F的GC耗時在100ms左右壕鹉,每分鐘GC count是2,如果是一般的系統(tǒng)聋涨,這樣的GC性能還是很不錯晾浴,完全達(dá)不到調(diào)優(yōu)的必要,但是系統(tǒng)F是一個統(tǒng)計服務(wù)牍白,在高峰期QPS 達(dá)到9萬脊凰,對外提供單詞請求最大的耗時是50ms,如果每次GC耗時108ms茂腥,意味著在這段時間內(nèi)的請求將全部超時(大約有9720個請求超時), ?這是不能容忍的狸涌。
通常來說GC調(diào)優(yōu)的目標(biāo)有以下三個:
高可用,可用性達(dá)到幾個9最岗。
低延遲帕胆,請求必須多少毫秒內(nèi)完成響應(yīng)。
高吞吐般渡,每秒完成多少次事務(wù)懒豹。
明確系統(tǒng)需求之所以重要,是因為上述性能指標(biāo)間可能沖突驯用。比如通常情況下脸秽,縮小延遲的代價是降低吞吐量或者消耗更多的內(nèi)存或者兩者同時發(fā)生。
系統(tǒng)F主要關(guān)注的指標(biāo)是高可用和低延遲蝴乔,因此調(diào)整主要是以該兩項指標(biāo)為主记餐。
此次優(yōu)化目標(biāo): (高峰期GC耗時降低至20ms左右,每分鐘GC count為1~2個)
我們查看了下優(yōu)化前的系統(tǒng)參數(shù):
-Xms12g -Xmx12g -Xmn4g -XX:ParallelGCThreads=4
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
gc 日志:
[GC (Allocation Failure) ?[ParNew: 2520325K->3721K(2831168K), 0.1117350 secs] 2955342K->438993K(5976896K), 0.0120977 secs] [Times: user=0.00 sys=0.04, real=0.11 secs]
[GC (Allocation Failure) ?[ParNew: 2520329K->3852K(2831168K), 0.1117688 secs] 2955601K->439390K(5976896K), 0.0121465 secs] [Times: user=0.01 sys=0.04, real=0.11 secs]
GC調(diào)整前的日志情況如下:
高峰期每分鐘GC count 2~3個
GC平均耗時在100ms
YGC階段回收效率超90%(說明年輕代中的對象都是朝生夕滅的薇正,大部分都被回收了)
幾乎沒有full gc
因為幾乎沒有出現(xiàn)full gc 說明老年大設(shè)置的有點(diǎn)大,可以適當(dāng)調(diào)整小點(diǎn)剥扣,那是不是意味著在總堆不變的情況下巩剖,年輕代就可以設(shè)置大些呢?如果年輕代設(shè)置過大钠怯,單位時間內(nèi)YGC count數(shù)量會降低,但是YGC掃描的時間就會增大曙聂,從而單次GC的耗時就會增多晦炊。 所以折中之后,我們調(diào)整了GC了以下參數(shù)作為對比:
1. -xmx8g -xms8g -xmn4g
2. -xmx8g -xms8g -xmn3g
3. -xmx8g -xms8g -xmn5g
4. -xmx6g -xms6g -xmn3g
通過觀察幾天這幾組GC參數(shù)對比宁脊,最終第四組gc 參數(shù)表現(xiàn)最好,而且?guī)缀醪怀霈F(xiàn)full gc断国。最終我們選擇了 -xmx6g -xms6g -xmn3g 作為我們GC的系統(tǒng)參數(shù),省下的機(jī)器配置還可以繼續(xù)水平擴(kuò)展(線上我們用的是docker)榆苞,通過此次調(diào)整我們發(fā)現(xiàn)系統(tǒng)堆不是設(shè)置的越大越好稳衬,適當(dāng)?shù)恼{(diào)整效果反而會更棒(當(dāng)然這里也出現(xiàn)了小插曲,當(dāng)時還設(shè)置了一組非常激進(jìn)的參數(shù) -xmx8g -xms8g -xmn6g坐漏,高峰期YGC表現(xiàn)更好薄疚,但是在下午16:00左右 系統(tǒng)不同程度出現(xiàn)了full gc ,單次full gc 耗時了幾秒赊琳,在這幾秒內(nèi)的請求全部超時街夭,所以參數(shù)設(shè)置的時候需要考慮full gc的情況,老年代不能設(shè)置的過低躏筏,需要合理設(shè)置板丽,否則一旦出現(xiàn)full gc,對于整個系統(tǒng)將是一場災(zāi)難趁尼。)