- 哪些內(nèi)存需要回收
- 什么時候回收
- 如何回收
如何判斷對象是否存活
可達(dá)性分析算法(Reachability Analysis): 通過一系列"GC Roots"對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始往下搜索炭菌,搜索過的路徑稱為引用鏈(Reference Chain),當(dāng)任意一個對象到GC Roots沒有任何引用鏈相連時罪佳,則證明此對象不可用。
GC Roots對象包括以下幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)變量屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI(本地Native方法)引用的對象
引用包括:
- 強(qiáng)引用:Java代碼中類似Object obj = new Object()這類的引用黑低。只要有強(qiáng)引用在赘艳,垃圾收集器永遠(yuǎn)不會回收掉被引用的對象
- 軟引用:用來描述一些還有用但并非必要的對象。對于軟引用關(guān)聯(lián)著的對象克握,在系統(tǒng)發(fā)生內(nèi)存溢出之前蕾管,將會把這些對象列進(jìn)回收范圍之中進(jìn)行二次回收。如果這次回收還沒有足夠的內(nèi)存菩暗,才會拋出內(nèi)存溢出異常掰曾。
- 弱引用:比軟引用更弱一些,只能存活到垃圾收集活動發(fā)生之前停团。
- 虛引用:它是最弱的一種引用關(guān)系旷坦,它存在的目的就是能在這個對象被收集器回收時收到系統(tǒng)通知。
對象的死亡
在可達(dá)性分析算法中不可達(dá)的對象佑稠,至少要經(jīng)歷兩次標(biāo)記的過程秒梅,才確定是否被回收:
1、如果對象在可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連的引用鏈舌胶,那它將會被第一次標(biāo)記并進(jìn)行一次篩選捆蜀,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。當(dāng)對象沒有覆蓋finalize()方法時辆琅,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過漱办,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”。
2婉烟、如果對象有必要執(zhí)行finalize()方法娩井,那么這個對象將會被放在一個F—Queue隊(duì)列之中,虛擬機(jī)的finalizer線程會執(zhí)行它
3似袁、finalize()方法是對象逃脫死亡的最后一次機(jī)會洞辣,稍后GC將對F-Queue隊(duì)列中的對象進(jìn)行第二次小規(guī)模的標(biāo)記,如果對象成功在finalize()中拯救自己(建立與引用鏈的聯(lián)系)昙衅,那在第二次標(biāo)記時它將被移除出“即將回收”的集合扬霜;否則它就要被回收。
方法區(qū)的回收(永久代)
方法區(qū)垃圾收集的效率是比較低的而涉,收集的內(nèi)容主要有兩部分:廢棄的常量和無用的類
無用的類需要符合三個條件:
- 該類的所有實(shí)例都已經(jīng)被回收
- 加載該類的ClassLoader已經(jīng)被回收
- 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用著瓶,無法在任何地方通過反射訪問該類的方法
垃圾收集算法
標(biāo)記-清除算法
標(biāo)記-清除算法分兩階段:首先標(biāo)記所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象
標(biāo)記-清除算法的不足:第一兩個過程的效率都不高啼县;第二會產(chǎn)生內(nèi)存碎片
復(fù)制算法
復(fù)制算法是把內(nèi)存分成相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊內(nèi)存用完了坛缕,就將還存活的對象復(fù)制到另外一塊內(nèi)存上,然后將當(dāng)前這塊內(nèi)存一次清理掉卷胯。
復(fù)制算法的好處就是實(shí)現(xiàn)簡單,效率高威酒,不足之處就是浪費(fèi)內(nèi)存空間窑睁。
如果對象的存活率較高,復(fù)制操作的效率會比較低葵孤,所以復(fù)制算法適合對象存活率較低的情況担钮。
現(xiàn)在商業(yè)虛擬機(jī)及HotSpot虛擬機(jī)都采用這種算法來回收新生代。新生代的內(nèi)存空間會分為Eden尤仍、From Survivor和To Survivor三塊裳朋,每次使用Eden空間和From Survivor空間。當(dāng)進(jìn)行回收時吓著,將Eden空間及From Survivor空間存活的對象復(fù)制到To Survivor,然后清理Eden和From Survivor空間。
標(biāo)記-整理算法
標(biāo)記-整理算法分兩階段:首先標(biāo)記需要回收的對象送挑,然后讓所有存活對象往一端移動绑莺,然后直接清理端邊界以外的內(nèi)存。
標(biāo)記-整理算法適合對象存活率較高的情況惕耕,并且不會產(chǎn)生內(nèi)存碎片
分代收集算法(Generational Collection)
一般把Java堆分成新生代和老年代:
新生代的對象存活率較低纺裁,使用復(fù)制算法
老年代的對象存活率較高,使用“標(biāo)記-清除”或“標(biāo)記-整理”算法
HotSpot的算法實(shí)現(xiàn)
枚舉根節(jié)點(diǎn)
必須確彼九欤可達(dá)性分析執(zhí)行的效率:HotSpot虛擬機(jī)中使用一組OopMap(Ordinary Object Pointer)普通對象指針存放對象的引用欺缘。在類加載完成的時候,HotSpot就把對象內(nèi)什么偏移量是什么類型的數(shù)據(jù)計(jì)算出來挤安,在JIT編譯過程中谚殊,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。
必須確备蛲可達(dá)性分析的執(zhí)行時在“一致性”的快照中進(jìn)行:在整個分析期間整個執(zhí)行系統(tǒng)看起來像被凍結(jié)在某個時間點(diǎn)上(Stop The World)嫩絮。
安全點(diǎn)(Safepoint)
安全點(diǎn)是HotSpot生成OopMap的位置,也是程序停下來GC的地方围肥。安全點(diǎn)的選定基本是以程序“是否具有讓程序長時間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定剿干,“長時間執(zhí)行”的最明顯特征就是指令序列復(fù)用,例如方法的調(diào)用穆刻、循環(huán)跳轉(zhuǎn)置尔、異常跳轉(zhuǎn)等。
如果在GC時讓所有的線程跑到安全點(diǎn)再停頓:
- 搶先式中斷:首先讓所有線程中斷氢伟,如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上榜轿,就恢復(fù)線程幽歼,讓它“跑”到安全點(diǎn)。目前基本沒有虛擬機(jī)才用這種方式
- 主動式中斷:當(dāng)GC需要中斷線程時差导,不直接對線程操作试躏,僅僅簡單地設(shè)置一個標(biāo)志,各個線程執(zhí)行時主動輪詢這個標(biāo)志设褐,發(fā)現(xiàn)中斷標(biāo)識為真時就自己中斷掛起颠蕴。輪詢標(biāo)志的地方和安全點(diǎn)是重合的,另外再加上創(chuàng)建對象需要分配內(nèi)存的地方助析。
安全區(qū)域(Safe Region)
安全區(qū)域是安全點(diǎn)的擴(kuò)展犀被,解決了程序在“不執(zhí)行”時候的GC安全問題:如果程序處在Sleep或Blocked的狀態(tài),這個時候線程無法響應(yīng)JVM的中斷請求外冀,無法走到安全點(diǎn)寡键。
在線程執(zhí)行到Safe Region中的代碼時,首先標(biāo)識自己已經(jīng)進(jìn)入了Safe Region雪隧。這樣西轩,當(dāng)這段時間內(nèi)發(fā)生GC時,就不用管標(biāo)識自己為Safe Region狀態(tài)的線程了脑沿。在線程要離開Safe Region時藕畔,它要檢查系統(tǒng)是否已經(jīng)完成根節(jié)點(diǎn)枚舉(整個GC過程),如果完成庄拇,線程就繼續(xù)執(zhí)行將注服,否則它就必須等待直到收到可以安全離開Safe Region的信號為止。
垃圾收集器
- 并行(Parallel):指多條垃圾收集線程并行工作措近,但此時用戶線程仍然處于等待情況
- 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時執(zhí)行(但不一定并行溶弟,可能會交替執(zhí)行),用戶程序繼續(xù)瞭郑,而垃圾收集程序運(yùn)行于另外一個CPU上
Serail辜御、ParNew、Parallel Scavenge : 復(fù)制算法
CMS:標(biāo)記-清除算法
Parallel Old屈张、Serial Old我抠、G1:標(biāo)記-整理算法
Serial 收集器
Seral 是一個單線程的收集器。它只會使用一個CPU或者一條收集線程去完成垃圾收集的工作袜茧,在它進(jìn)行垃圾收集時菜拓,必須暫停其他所有的工作線程。
多CPU的情況下笛厦,效率比較低纳鼎。但對于單CPU的情況下,因?yàn)槭菃尉€程,減少了線程之間的交互贱鄙,專注垃圾收集的工作劝贸,效率會比較高,適用于Client模式下的虛擬機(jī)逗宁。
ParNew 收集器
ParNew是Serial的多線程版映九。除了使用多條線程進(jìn)行垃圾收集之外,其余行為都和Serial一樣瞎颗。它默認(rèn)開啟的收集線程和CPU的數(shù)量相同件甥,在CPU非常多的情況下,可以通過參數(shù)-XX:ParallelGCThreads限制垃圾收集的線程數(shù)哼拔。ParNew是目前唯一能和CMS配合工作的收集器引有。
在單CPU的情況下,因?yàn)榇嬖诰€程交互的開銷倦逐,ParNew的性能不一定比Serial收集器的好譬正。不過在當(dāng)前CPU動輒4核的情況下,它是許多運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器檬姥。
使用-XX:UserConcMarkSweepGC或者-XX:UserParNewGC選項(xiàng)來指定
Parallel Scavenge 收集器
Parallel Scavenge 是一個并行的多線程收集器曾我,它的目標(biāo)是達(dá)到一個可控的吞吐量(Throughput),因此也被稱為“吞吐量優(yōu)先”收集器健民。
吞吐量就是CPU用于運(yùn)行用戶代碼的時間與CPU總消耗時間的比值您单,吞吐量=運(yùn)行用戶代碼時間/(運(yùn)行用戶代碼時間+垃圾收集時間)。
停頓時間越短就越適合需要與用戶交互的程序荞雏,良好的響應(yīng)速度能提升用戶的體驗(yàn),而高吞吐量則可以高效地利用CPU時間平酿,盡快地完成程序的運(yùn)算任務(wù)凤优,主要適合后臺運(yùn)算而不需要太多交互的任務(wù)。
Parallel Scavenge收集器提供兩個參數(shù)用于精確控制吞吐量:
- -XX:MaxGCPauseMillis:最大的GC停頓時間蜈彼。允許一個大于0的毫秒數(shù)筑辨,收集器盡可能保證內(nèi)存回收花費(fèi)的時間不超過設(shè)定值。但這并不意味這個值越小越好幸逆,因?yàn)镚C停頓時間縮短是以犧牲吞吐量和新生代空間來換取的棍辕。因?yàn)槭占?00M的新生代肯定比收集500M的新生代快,原來10秒收集一次还绘,每次停頓100毫秒楚昭,現(xiàn)在5秒收集一次,每次停頓70毫米拍顷,停頓時間是下降了抚太,但吞吐量也降低了。
- -XX:GCTimeRatio:垃圾收集時間戰(zhàn)總時間的比率,相當(dāng)于是吞吐量的倒數(shù)尿贫。如果把此參數(shù)設(shè)置為19电媳,那允許的最大GC時間就占5%(1/(1+19)),默認(rèn)值是99,就是允許最大1%(1/(1+99))的垃圾收集時間庆亡。
GC自適應(yīng)調(diào)節(jié)策略(GC Ergonomics):-XX:UseAdaptiveSizePolicy 當(dāng)這個參數(shù)打開匾乓,就不需要手工指定新生代的大小(-Xmn)又谋、Eden與Survivor的比例(-XX:SurvivorRatio)拼缝、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù),虛擬機(jī)會根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控搂根,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量珍促。
Serial Old 收集器
Serial Old 是Serail收集器的老年代版本,適合于給Client模式下的虛擬機(jī)使用剩愧。如果在Server模式下:一種用途是在JDK1.5及之前的版本和Parallel Scavenge搭配使用猪叙;另一種用途就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用
Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本仁卷,使用多線程和“標(biāo)記-整理”算法穴翩,在JDK1.6中才開始提供。
CMS(Concurrent Mark Sweep) 收集器
CMS 收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器锦积。目前大部分應(yīng)用在互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端上芒帕,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)的停頓時間最短丰介,以給用戶帶來較好的體驗(yàn)背蟆。
CMS收集器基于“標(biāo)記-清除”算法實(shí)現(xiàn),整個過程分為4步驟:
- 初始標(biāo)記(CMS inital mark) 需要“Stop The World”哮幢,僅僅就只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象
- 并發(fā)標(biāo)記(CMS Concurrent mark) GC Roots枚舉階段
- 重新標(biāo)記(CMS remark) 需要“Stop The World”带膀,該階段是修正并發(fā)標(biāo)記期間因?yàn)橛脩艟€程繼續(xù)運(yùn)作導(dǎo)致標(biāo)記產(chǎn)生變動的那部分對象的標(biāo)記記錄
- 并發(fā)清除(CMS concurrent sweep)
CMS收集器提供了并發(fā)收集、低停頓等優(yōu)點(diǎn)橙垢,但也有很明顯的缺點(diǎn):
1垛叨、CMS收集器對CPU資源敏感:CMS默認(rèn)啟動的回收線程數(shù)是(CPU數(shù)量+3)/4,也就是當(dāng)CPU在4個以上時柜某,并發(fā)回收時垃圾收集線程占不少于25%的CPU資源嗽元,并且隨著CPU數(shù)量的增加而降低。但如果CPU少于4個時(比如2個時)喂击,CMS對用戶程序的影響就會比較大剂癌。
2、CMS收集器無法處理浮動(Float Garbage)垃圾,可能出現(xiàn)“Concurrent Mode Failure”失敗導(dǎo)致發(fā)生另外一次Full GC翰绊。
CMS在并發(fā)清理階段用戶線性還在運(yùn)行珍手,伴隨用戶線程的運(yùn)行自然就會有新的垃圾不斷產(chǎn)生,CMS無法在當(dāng)次處理這些垃圾,只能在下次GC時處理琳要,這些垃圾稱為“浮動垃圾”寡具。
由于用戶線程在GC的時候還在運(yùn)行,所以必須預(yù)留足夠的內(nèi)存空間給用戶線程稚补,當(dāng)預(yù)留的內(nèi)存空間無法滿足用戶線程時童叠,就會出現(xiàn)“Concurrent Mode Failure”失敗,CMS啟動后備方案:臨時啟動Serial Old進(jìn)行老年代的垃圾收集课幕,此時厦坛,停頓時間就會比較長。CMS收集器通過參數(shù)-XX:CMSInitiatingOccupancyFraction 來激活自己乍惊,JDK1.5默認(rèn)設(shè)置當(dāng)老年代使用68%觸發(fā)CMS收集器杜秸,JDK1.6中,此值為92%润绎。
3撬碟、CMS收集器采用“標(biāo)記-清除”算法,會產(chǎn)生大量的空間碎片莉撇。會導(dǎo)致老年代還有很多空間呢蛤,因?yàn)檎也坏阶銐虼蟮倪B續(xù)空間不得不進(jìn)行Full GC。
- -XX:UseCMSCompactAtFullCollection:開關(guān)參數(shù)棍郎,默認(rèn)是開啟其障,用于在CMS收集器頂不住需要進(jìn)行Full GC是開啟內(nèi)存碎片整理,這樣會導(dǎo)致停頓時間變長
- -XX:CMSFullGCsBeforeCompaction:這個參數(shù)用于設(shè)置執(zhí)行多少次不壓縮的Full GC后涂佃,跟著來一次帶壓縮的励翼。默認(rèn)為0,表示每次進(jìn)入Full GC時都進(jìn)行內(nèi)存碎片整理
G1 收集器
G1是一款面向服務(wù)端應(yīng)用的垃圾收集器辜荠,它有以下幾個特點(diǎn):
- 并行與并發(fā):G1重復(fù)利用硬件資源優(yōu)勢汽抚,使用多個CPU核心來縮短Stop-The-World停頓時間
- 分代收集:分代的概念依然在G1中保留
- 空間整合:G1從整體上看采用了“標(biāo)記-整理”的算法,局部(連個Region)來看是基于復(fù)制算法侨拦,這樣保證了G1運(yùn)行期間不會產(chǎn)生內(nèi)存碎片
- 可預(yù)測停頓:G1除了最求低停頓外,還能建立可預(yù)測的停頓模型辐宾。能讓使用明確指定在一個長度為M毫秒的時間片段內(nèi)狱从,消耗在垃圾收集上的時間不超過N毫秒。
G1收集器Java堆的布局:將整Java堆劃分多個大小相等的獨(dú)立區(qū)域(Region),雖然還保留有新生代和老年代的概念叠纹,但新生代和老年代不再是物理隔離的了季研,他們都是一部分Region(不需要連續(xù))的集合。
G1創(chuàng)建可預(yù)測的停頓時間模型:G1跟蹤各個Region里面的垃圾堆積的價值大杏臁(回收所獲得的空間大小及回收所需要時間的經(jīng)驗(yàn)值)与涡,在后臺維護(hù)一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region驼卖,這也是Garbage-First名稱的來由氨肌。
G1如何避免全堆掃描:使用Remembered Set來避免全堆掃描。G1中的每個Region都有一個與之對應(yīng)的Remembered Set,虛擬機(jī)發(fā)現(xiàn)程序在對Reference類型的數(shù)據(jù)進(jìn)行寫操作時酌畜,會產(chǎn)生一個Write Barrier暫停中斷寫操作怎囚,檢查Reference引用的對象是否處于不同的Region之中(在分代的例子中就是是否有老年代的對象引用了新生代的對象)。如果是桥胞,便通過CardTable把相關(guān)引用信息記錄到被引用對象所屬的Region的Remembered Set之中恳守。當(dāng)進(jìn)行內(nèi)存回收時,在GC根節(jié)點(diǎn)的枚舉范圍假如Remembered Set即可贩虾。
G1收集器的回收步驟可分為以下步驟:
- 初始標(biāo)記(Initial Marking):僅僅標(biāo)記GC Roots能直接關(guān)聯(lián)到的對象催烘,并且修改TAMS(Next Top at Mark Start)的值,讓下一個階段用戶程序并發(fā)運(yùn)行時缎罢,能在正確的Region中創(chuàng)建新對象伊群,這個階段需要停頓用戶線程。
- 并發(fā)標(biāo)記(Concurrent Marking):從堆的GC RootS開始對堆的對象進(jìn)行可達(dá)性分析屁使,找出存活的對象在岂,這個階段用戶線程可并發(fā)執(zhí)行
- 最終標(biāo)記(Final Marking):修正由于并發(fā)階段用戶線程執(zhí)行運(yùn)行導(dǎo)致標(biāo)志產(chǎn)生變動的那一部分標(biāo)記記錄。虛擬機(jī)將這段時間對象變化記錄在線程Remembered Set Logs里面蛮寂,最終標(biāo)記階段需要把Remembered Set Logs的數(shù)據(jù)合并到Remembered Set中蔽午。這階段需要停頓用戶線程,當(dāng)標(biāo)記線程可以并行執(zhí)行酬蹋。
- 篩選回收(Live Data Counting and Evacuating ):首先對各個Region的回收價值和成本進(jìn)行排序及老,根據(jù)用戶所期望的GC停頓時間來指定回收計(jì)劃。
內(nèi)存的分配與回收策略
對象優(yōu)先在Eden分配
- 新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動作范抓,因?yàn)镴ava對象大多數(shù)具備朝生夕滅的特征骄恶,所以Minor GC非常頻繁,一般回收速度也很快匕垫。
- 老年代(Major GC/Full GC): 指發(fā)生在老年代的GC僧鲁,出現(xiàn)了Major GC,經(jīng)常會伴隨至少一次的Minor GC(Parallel Scavenge收集器例外)象泵,Major GC 一般比Minor GC慢10倍以上寞秃。
大對象直接進(jìn)入老年代
大對象:需要大量連續(xù)空間的Java對象,比如:長的字符串和數(shù)組偶惠。對大對象春寿,我們可以通過參數(shù)設(shè)置,讓它直接在老年代分配忽孽,從而避免頻繁的Minor GC绑改。
-XX:PretenureSizeThreshold:可以讓對象直接在老年代分配
長期存活的對象進(jìn)入老年代
對象年齡技術(shù)器:虛擬機(jī)給每個對象定義了一個對象年齡計(jì)數(shù)器谢床。如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor容納的話厘线,將被移動到Survivor空間识腿,并且對象的年齡設(shè)定為1。對象在Survivor區(qū)每熬過一次Minor GC皆的,年齡就增加1覆履。當(dāng)它的年齡增加到一定程度(默認(rèn)為15),就將會晉升到老年代费薄。對象晉升到老年代的閥值硝全,可以通過參數(shù)-XX:MaxTenuringThreshold設(shè)置。
動態(tài)對象年齡判斷
虛擬機(jī)并不是永遠(yuǎn)要求對象的年齡必須達(dá)到MaxTenuringThreshold才能晉升老年代楞抡。如果在Survivor空間中相同年齡的所有對象大小的總和大于Survivor空間的一半伟众,年齡大于或者等于該年齡的對象就可以直接進(jìn)入老年代。
空間分配擔(dān)保
在發(fā)生Minor GC之前召廷,虛擬機(jī)會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間凳厢,如果條件成立,那么Minor GC可以確保是安全的竞慢。如果不成立先紫,則虛擬機(jī)會查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。如果允許筹煮,那么繼續(xù)檢查老年代最大可用連續(xù)空間是否大于歷次晉升老年代對象的平均大小遮精,如果大于,將嘗試進(jìn)行一次Minor GC败潦;如果小于本冲,或者HandlePromotionFailure設(shè)置不允許,需要進(jìn)行一次Full GC劫扒。
但是檬洞,在JDK1.6 Update24之后,規(guī)則變?yōu)橹灰夏甏倪B續(xù)空間大于新生代對象總大小或者歷次晉升的平均大小就會進(jìn)行Minor GC沟饥,否則將進(jìn)行Full GC添怔。
理解GC日志
打印GC日志:-XX:PrintGCDetails
GC日志 待補(bǔ)充……