本文主要講述JVM中幾種常見(jiàn)的垃圾回收算法和相關(guān)的垃圾回收器忙干,以及常見(jiàn)的和GC相關(guān)的性能調(diào)優(yōu)參數(shù)。
GC Roots
我們先來(lái)了解一下在Java中是如何判斷一個(gè)對(duì)象的生死的浪藻,有些語(yǔ)言比如Python是采用引用計(jì)數(shù)來(lái)統(tǒng)計(jì)的捐迫,但是這種做法可能會(huì)遇見(jiàn)循環(huán)引用的問(wèn)題,在Java以及C#等語(yǔ)言中是采用GC Roots來(lái)解決這個(gè)問(wèn)題爱葵。如果一個(gè)對(duì)象和GC Roots之間沒(méi)有鏈接施戴,那么這個(gè)對(duì)象也可以被視作是一個(gè)可回收的對(duì)象。
Java中可以被作為GC Roots中的對(duì)象有:
- 虛擬機(jī)棧中的引用的對(duì)象萌丈。
- 方法區(qū)中的類(lèi)靜態(tài)屬性引用的對(duì)象赞哗。
- 方法區(qū)中的常量引用的對(duì)象。
- 本地方法棧(jni)即一般說(shuō)的Native的引用對(duì)象浓瞪。
標(biāo)記清除
標(biāo)記-清除算法將垃圾回收分為兩個(gè)階段:標(biāo)記階段和清除階段懈玻。在標(biāo)記階段首先通過(guò)根節(jié)點(diǎn),標(biāo)記所有從根節(jié)點(diǎn)開(kāi)始的對(duì)象乾颁,未被標(biāo)記的對(duì)象就是未被引用的垃圾對(duì)象涂乌。然后,在清除階段英岭,清除所有未被標(biāo)記的對(duì)象湾盒。標(biāo)記清除算法帶來(lái)的一個(gè)問(wèn)題是會(huì)存在大量的空間碎片,因?yàn)榛厥蘸蟮目臻g是不連續(xù)的诅妹,這樣給大對(duì)象分配內(nèi)存的時(shí)候可能會(huì)提前觸發(fā)full gc罚勾。
標(biāo)記清除
復(fù)制算法
將現(xiàn)有的內(nèi)存空間分為兩快,每次只使用其中一塊吭狡,在垃圾回收時(shí)將正在使用的內(nèi)存中的存活對(duì)象復(fù)制到未被使用的內(nèi)存塊中尖殃,之后,清除正在使用的內(nèi)存塊中的所有對(duì)象划煮,交換兩個(gè)內(nèi)存的角色送丰,完成垃圾回收。
復(fù)制算法
現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來(lái)回收新生代弛秋,IBM研究表明新生代中的對(duì)象98%是朝夕生死的器躏,所以并不需要按照1:1的比例劃分內(nèi)存空間俐载,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor登失。當(dāng)回收時(shí)遏佣,將Eden和Survivor中還存活著的對(duì)象一次性地拷貝到另外一個(gè)Survivor空間上,最后清理掉Eden和剛才用過(guò)的Survivor的空間揽浙。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1(可以通過(guò)-SurvivorRattio來(lái)配置)状婶,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%,只有10%的內(nèi)存會(huì)被“浪費(fèi)”馅巷。當(dāng)然太抓,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),我們沒(méi)有辦法保證回收都只有不多于10%的對(duì)象存活令杈,當(dāng)Survivor空間不夠用時(shí),需要依賴(lài)其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保碴倾。
標(biāo)記整理
復(fù)制算法的高效性是建立在存活對(duì)象少逗噩、垃圾對(duì)象多的前提下的。這種情況在新生代經(jīng)常發(fā)生跌榔,但是在老年代更常見(jiàn)的情況是大部分對(duì)象都是存活對(duì)象异雁。如果依然使用復(fù)制算法,由于存活的對(duì)象較多僧须,復(fù)制的成本也將很高纲刀。
標(biāo)記整理
標(biāo)記-壓縮算法是一種老年代的回收算法,它在標(biāo)記-清除算法的基礎(chǔ)上做了一些優(yōu)化担平。首先也需要從根節(jié)點(diǎn)開(kāi)始對(duì)所有可達(dá)對(duì)象做一次標(biāo)記示绊,但之后,它并不簡(jiǎn)單地清理未標(biāo)記的對(duì)象暂论,而是將所有的存活對(duì)象壓縮到內(nèi)存的一端面褐。之后,清理邊界外所有的空間取胎。這種方法既避免了碎片的產(chǎn)生展哭,又不需要兩塊相同的內(nèi)存空間,因此闻蛀,其性?xún)r(jià)比比較高匪傍。
增量算法
增量算法的基本思想是,如果一次性將所有的垃圾進(jìn)行處理觉痛,需要造成系統(tǒng)長(zhǎng)時(shí)間的停頓役衡,那么就可以讓垃圾收集線程和應(yīng)用程序線程交替執(zhí)行。每次秧饮,垃圾收集線程只收集一小片區(qū)域的內(nèi)存空間映挂,接著切換到應(yīng)用程序線程泽篮。依次反復(fù),直到垃圾收集完成柑船。使用這種方式帽撑,由于在垃圾回收過(guò)程中,間斷性地還執(zhí)行了應(yīng)用程序代碼鞍时,所以能減少系統(tǒng)的停頓時(shí)間亏拉。但是,因?yàn)榫€程切換和上下文轉(zhuǎn)換的消耗逆巍,會(huì)使得垃圾回收的總體成本上升及塘,造成系統(tǒng)吞吐量的下降。
垃圾回收器
Serial收集器
Serial收集器是最古老的收集器锐极,它的缺點(diǎn)是當(dāng)Serial收集器想進(jìn)行垃圾回收的時(shí)候笙僚,必須暫停用戶(hù)的所有進(jìn)程,即stop the world灵再。到現(xiàn)在為止肋层,它依然是虛擬機(jī)運(yùn)行在client模式下的默認(rèn)新生代收集器,與其他收集器相比翎迁,對(duì)于限定在單個(gè)CPU的運(yùn)行環(huán)境來(lái)說(shuō)栋猖,Serial收集器由于沒(méi)有線程交互的開(kāi)銷(xiāo),專(zhuān)心做垃圾回收自然可以獲得最高的單線程收集效率汪榔。
Serial Old是Serial收集器的老年代版本蒲拉,它同樣是一個(gè)單線程收集器,使用”標(biāo)記-整理“算法痴腌。這個(gè)收集器的主要意義也是被Client模式下的虛擬機(jī)使用雌团。在Server模式下,它主要還有兩大用途:一個(gè)是在JDK1.5及以前的版本中與Parallel Scanvenge收集器搭配使用衷掷,另外一個(gè)就是作為CMS收集器的后備預(yù)案辱姨,在并發(fā)收集發(fā)生Concurrent Mode Failure的時(shí)候使用。
通過(guò)指定-UseSerialGC
參數(shù)戚嗅,使用Serial + Serial Old的串行收集器組合進(jìn)行內(nèi)存回收雨涛。
ParNew收集器
ParNew收集器是Serial收集器新生代的多線程實(shí)現(xiàn),注意在進(jìn)行垃圾回收的時(shí)候依然會(huì)stop the world懦胞,只是相比較Serial收集器而言它會(huì)運(yùn)行多條進(jìn)程進(jìn)行垃圾回收替久。
ParNew收集器在單CPU的環(huán)境中絕對(duì)不會(huì)有比Serial收集器更好的效果,甚至由于存在線程交互的開(kāi)銷(xiāo)躏尉,該收集器在通過(guò)超線程技術(shù)實(shí)現(xiàn)的兩個(gè)CPU的環(huán)境中都不能百分之百的保證能超越Serial收集器蚯根。當(dāng)然,隨著可以使用的CPU的數(shù)量增加,它對(duì)于GC時(shí)系統(tǒng)資源的利用還是很有好處的颅拦。它默認(rèn)開(kāi)啟的收集線程數(shù)與CPU的數(shù)量相同蒂誉,在CPU非常多(譬如32個(gè),現(xiàn)在CPU動(dòng)輒4核加超線程距帅,服務(wù)器超過(guò)32個(gè)邏輯CPU的情況越來(lái)越多了)的環(huán)境下右锨,可以使用-XX:ParallelGCThreads
參數(shù)來(lái)限制垃圾收集的線程數(shù)。
-UseParNewGC
: 打開(kāi)此開(kāi)關(guān)后碌秸,使用ParNew + Serial Old的收集器組合進(jìn)行內(nèi)存回收绍移,這樣新生代使用并行收集器,老年代使用串行收集器讥电。
Parallel Scavenge收集器
Parallel是采用復(fù)制算法的多線程新生代垃圾回收器蹂窖,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一個(gè)特點(diǎn)是它所關(guān)注的目標(biāo)是吞吐量(Throughput)恩敌。所謂吞吐量就是CPU用于運(yùn)行用戶(hù)代碼的時(shí)間與CPU總消耗時(shí)間的比值瞬测,即吞吐量=運(yùn)行用戶(hù)代碼時(shí)間 / (運(yùn)行用戶(hù)代碼時(shí)間 + 垃圾收集時(shí)間)。停頓時(shí)間越短就越適合需要與用戶(hù)交互的程序纠炮,良好的響應(yīng)速度能夠提升用戶(hù)的體驗(yàn)涣楷;而高吞吐量則可以最高效率地利用CPU時(shí)間,盡快地完成程序的運(yùn)算任務(wù)抗碰,主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本绽乔,采用多線程和”標(biāo)記-整理”算法弧蝇。這個(gè)收集器是在jdk1.6中才開(kāi)始提供的,在此之前折砸,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態(tài)看疗。原因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外別無(wú)選擇睦授。由于單線程的老年代Serial Old收集器在服務(wù)端應(yīng)用性能上的”拖累“两芳,即使使用了Parallel Scavenge收集器也未必能在整體應(yīng)用上獲得吞吐量最大化的效果,又因?yàn)槔夏甏占袩o(wú)法充分利用服務(wù)器多CPU的處理能力去枷,在老年代很大而且硬件比較高級(jí)的環(huán)境中怖辆,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合”給力“。直到Parallel Old收集器出現(xiàn)后删顶,”吞吐量?jī)?yōu)先“收集器終于有了比較名副其實(shí)的應(yīng)用竖螃,在注重吞吐量及CPU資源敏感的場(chǎng)合,都可以?xún)?yōu)先考慮Parallel Scavenge加Parallel Old收集器逗余。
-UseParallelGC
: 虛擬機(jī)運(yùn)行在Server模式下的默認(rèn)值特咆,打開(kāi)此開(kāi)關(guān)后,使用Parallel Scavenge + Serial Old的收集器組合進(jìn)行內(nèi)存回收录粱。-UseParallelOldGC: 打開(kāi)此開(kāi)關(guān)后腻格,使用Parallel Scavenge + Parallel Old的收集器組合進(jìn)行垃圾回收
CMS收集器
CMS(Concurrent Mark Swep)收集器是一個(gè)比較重要的回收器画拾,現(xiàn)在應(yīng)用非常廣泛佳鳖,我們重點(diǎn)來(lái)看一下宇葱,CMS一種獲取最短回收停頓時(shí)間為目標(biāo)的收集器蛹含,這使得它很適合用于和用戶(hù)交互的業(yè)務(wù)玄括。從名字(Mark Swep)就可以看出帽衙,CMS收集器是基于標(biāo)記清除算法實(shí)現(xiàn)的掂碱。它的收集過(guò)程分為四個(gè)步驟:
- 初始標(biāo)記(initial mark)
- 并發(fā)標(biāo)記(concurrent mark)
- 重新標(biāo)記(remark)
- 并發(fā)清除(concurrent sweep)
注意初始標(biāo)記和重新標(biāo)記還是會(huì)stop the world敲街,但是在耗費(fèi)時(shí)間更長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除兩個(gè)階段都可以和用戶(hù)進(jìn)程同時(shí)工作扫腺。
不過(guò)由于CMS收集器是基于標(biāo)記清除算法實(shí)現(xiàn)的愁茁,會(huì)導(dǎo)致有大量的空間碎片產(chǎn)生蚕钦,在為大對(duì)象分配內(nèi)存的時(shí)候,往往會(huì)出現(xiàn)老年代還有很大的空間剩余鹅很,但是無(wú)法找到足夠大的連續(xù)空間來(lái)分配當(dāng)前對(duì)象嘶居,不得不提前開(kāi)啟一次Full GC。為了解決這個(gè)問(wèn)題促煮,CMS收集器默認(rèn)提供了一個(gè)-XX:+UseCMSCompactAtFullCollection收集開(kāi)關(guān)參數(shù)(默認(rèn)就是開(kāi)啟的)邮屁,用于在CMS收集器進(jìn)行FullGC完開(kāi)啟內(nèi)存碎片的合并整理過(guò)程,內(nèi)存整理的過(guò)程是無(wú)法并發(fā)的菠齿,這樣內(nèi)存碎片問(wèn)題倒是沒(méi)有了佑吝,不過(guò)停頓時(shí)間不得不變長(zhǎng)。虛擬機(jī)設(shè)計(jì)者還提供了另外一個(gè)參數(shù)-XX:CMSFullGCsBeforeCompaction參數(shù)用于設(shè)置執(zhí)行多少次不壓縮的FULL GC后跟著來(lái)一次帶壓縮的(默認(rèn)值為0绳匀,表示每次進(jìn)入Full GC時(shí)都進(jìn)行碎片整理)芋忿。
不幸的是,它作為老年代的收集器疾棵,卻無(wú)法與jdk1.4中已經(jīng)存在的新生代收集器Parallel Scavenge配合工作戈钢,所以在jdk1.5中使用cms來(lái)收集老年代的時(shí)候,新生代只能選擇ParNew或Serial收集器中的一個(gè)是尔。ParNew收集器是使用-XX:+UseConcMarkSweepGC選項(xiàng)啟用CMS收集器之后的默認(rèn)新生代收集器殉了,也可以使用-XX:+UseParNewGC選項(xiàng)來(lái)強(qiáng)制指定它。
由于CMS收集器現(xiàn)在比較常用拟枚,下面我們?cè)兕~外了解一下CMS算法的幾個(gè)常用參數(shù):
- UseCMSInitatingOccupancyOnly:表示只在到達(dá)閾值的時(shí)候薪铜,才進(jìn)行 CMS 回收。
- CMS默認(rèn)啟動(dòng)的回收線程數(shù)目是(ParallelGCThreads+3)/4恩溅,如果你需要明確設(shè)定痕囱,可以通過(guò)-XX:+ParallelCMSThreads來(lái)設(shè)定,其中-XX:+ParallelGCThreads代表的年輕代的并發(fā)收集線程數(shù)目暴匠。
- CMSClassUnloadingEnabled: 允許對(duì)元類(lèi)數(shù)據(jù)進(jìn)行回收鞍恢。
- CMSInitatingPermOccupancyFraction:當(dāng)永久區(qū)占用率達(dá)到這一百分比后,啟動(dòng) CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。
- CMSIncrementalMode:使用增量模式帮掉,比較適合單 CPU弦悉。
- UseCMSCompactAtFullCollection參數(shù)可以使 CMS 在垃圾收集完成后,進(jìn)行一次內(nèi)存碎片整理蟆炊。內(nèi)存碎片的整理并不是并發(fā)進(jìn)行的稽莉。
- UseFullGCsBeforeCompaction:設(shè)定進(jìn)行多少次 CMS 垃圾回收后,進(jìn)行一次內(nèi)存壓縮涩搓。
一些建議
對(duì)于Native Memory:
- 使用了NIO或者NIO框架(Mina/Netty)
- 使用了DirectByteBuffer分配字節(jié)緩沖區(qū)
- 使用了MappedByteBuffer做內(nèi)存映射
- 由于Native Memory只能通過(guò)FullGC回收污秆,所以除非你非常清楚這時(shí)真的有必要,否則不要輕易調(diào)用System.gc()昧甘。
另外為了防止某些框架中的System.gc調(diào)用(例如NIO框架良拼、Java RMI),建議在啟動(dòng)參數(shù)中加上-XX:+DisableExplicitGC來(lái)禁用顯式GC充边。這個(gè)參數(shù)有個(gè)巨大的坑庸推,如果你禁用了System.gc(),那么上面的3種場(chǎng)景下的內(nèi)存就無(wú)法回收浇冰,可能造成OOM贬媒,如果你使用了CMS GC,那么可以用這個(gè)參數(shù)替代:-XX:+ExplicitGCInvokesConcurrent肘习。
此外除了CMS的GC际乘,其實(shí)其他針對(duì)old gen的回收器都會(huì)在對(duì)old gen回收的同時(shí)回收young gen。
G1收集器
G1收集器是一款面向服務(wù)端應(yīng)用的垃圾收集器漂佩。HotSpot團(tuán)隊(duì)賦予它的使命是在未來(lái)替換掉JDK1.5中發(fā)布的CMS收集器蚓庭。與其他GC收集器相比,G1具備如下特點(diǎn):
- 并行與并發(fā):G1能更充分的利用CPU仅仆,多核環(huán)境下的硬件優(yōu)勢(shì)來(lái)縮短stop the world的停頓時(shí)間。
- 分代收集:和其他收集器一樣垢袱,分代的概念在G1中依然存在墓拜,不過(guò)G1不需要其他的垃圾回收器的配合就可以獨(dú)自管理整個(gè)GC堆。
- 空間整合:G1收集器有利于程序長(zhǎng)時(shí)間運(yùn)行请契,分配大對(duì)象時(shí)不會(huì)無(wú)法得到連續(xù)的空間而提前觸發(fā)一次GC咳榜。
可預(yù)測(cè)的非停頓:這是G1相對(duì)于CMS的另一大優(yōu)勢(shì),降低停頓時(shí)間是G1和CMS共同的關(guān)注點(diǎn)爽锥,能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi)涌韩,消耗在垃圾收集上的時(shí)間不得超過(guò)N毫秒。 - 在使用G1收集器時(shí)氯夷,Java堆的內(nèi)存布局和其他收集器有很大的差別臣樱,它將這個(gè)Java堆分為多個(gè)大小相等的獨(dú)立區(qū)域,雖然還保留新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了雇毫,它們都是一部分Region(不需要連續(xù))的集合玄捕。
雖然G1看起來(lái)有很多優(yōu)點(diǎn),實(shí)際上CMS還是主流棚放。
與GC相關(guān)的常用參數(shù)
除了上面提及的一些參數(shù)枚粘,下面補(bǔ)充一些和GC相關(guān)的常用參數(shù):
- Xmx: 設(shè)置堆內(nèi)存的最大值。
- Xms: 設(shè)置堆內(nèi)存的初始值飘蚯。
- Xmn: 設(shè)置新生代的大小馍迄。
- Xss: 設(shè)置棧的大小。
- PretenureSizeThreshold: 直接晉升到老年代的對(duì)象大小局骤,設(shè)置這個(gè)參數(shù)后攀圈,大于這個(gè)參數(shù)的對(duì)象將直接在老年代分配。
- MaxTenuringThrehold: 晉升到老年代的對(duì)象年齡庄涡。每個(gè)對(duì)象在堅(jiān)持過(guò)一次Minor GC之后量承,年齡就會(huì)加1,當(dāng)超過(guò)這個(gè)參數(shù)值時(shí)就進(jìn)入老年代穴店。
- UseAdaptiveSizePolicy: 在這種模式下撕捍,新生代的大小、eden 和 survivor 的比例泣洞、晉升老年代的對(duì)象年齡等參數(shù)會(huì)被自動(dòng)調(diào)整忧风,以達(dá)到在堆大小、吞吐量和停頓時(shí)間之間的平衡點(diǎn)球凰。在手工調(diào)優(yōu)比較困難的場(chǎng)合狮腿,可以直接使用這種自適應(yīng)的方式,僅指定虛擬機(jī)的最大堆呕诉、目標(biāo)的吞吐量 (GCTimeRatio) 和停頓時(shí)間 (MaxGCPauseMills)缘厢,讓虛擬機(jī)自己完成調(diào)優(yōu)工作。
- SurvivorRattio: 新生代Eden區(qū)域與Survivor區(qū)域的容量比值甩挫,默認(rèn)為8贴硫,代表Eden: Suvivor= 8: 1。
- XX:ParallelGCThreads:設(shè)置用于垃圾回收的線程數(shù)伊者。通常情況下可以和 CPU 數(shù)量相等英遭。但在 CPU 數(shù)量比較多的情況下,設(shè)置相對(duì)較小的數(shù)值也是合理的亦渗。
- XX:MaxGCPauseMills:設(shè)置最大垃圾收集停頓時(shí)間挖诸。它的值是一個(gè)大于 0 的整數(shù)。收集器在工作時(shí)法精,會(huì)調(diào)整 Java 堆大小或者其他一些參數(shù)多律,盡可能地把停頓時(shí)間控制在 MaxGCPauseMills 以?xún)?nèi)痴突。
- XX:GCTimeRatio:設(shè)置吞吐量大小,它的值是一個(gè) 0-100 之間的整數(shù)菱涤。假設(shè) GCTimeRatio 的值為 n苞也,那么系統(tǒng)將花費(fèi)不超過(guò) 1/(1+n) 的時(shí)間用于垃圾收集。