概述
內(nèi)存回收主要考慮三件事情
- 那些內(nèi)存需要回收鲫趁?
- 什么時候回收炕桨?
- 如何回收饭尝?
程序計數(shù)器、虛擬機(jī)棧献宫、本地方法棧三個區(qū)域隨線程而生钥平,隨線程而滅,每一個棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)定下來時就已知的姊途。因此這三個區(qū)域不需要過多的考慮回收的問題涉瘾,因?yàn)楫?dāng)方法結(jié)束或者線程結(jié)束是,內(nèi)存自然就跟著回收了捷兰。
而Java堆和方法區(qū)則不一樣立叛,一個接口中的多個實(shí)現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存也不一樣贡茅。而且我們只有在程序的運(yùn)行期間時才能知道創(chuàng)建哪些對象秘蛇,這部分的內(nèi)存分配和回收都是動態(tài)的,垃圾收集器關(guān)注的也是這部分顶考。
對象已死嗎赁还?
引用計數(shù)算法
給對象添加一個一弄計數(shù)器,每當(dāng)有一個地方引用它時村怪,計數(shù)器值就加1秽浇;當(dāng)引用失效時浮庐,計數(shù)器值就減1甚负;任何時刻計數(shù)器為0的對象就是不可能再被使用的。但是如果兩個對象互相引用审残,引用計數(shù)算法就無法判斷這兩個對象是否存活梭域。
可達(dá)性分析算法
通過一系列稱為“GC Roots”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索搅轿,搜索走過的路徑名稱為引用鏈(Reference Chain)病涨,當(dāng)一個對象到GC Roots沒有任何引用鏈時,則證明次對象不可用的璧坟。
在Java語言中既穆,可作為GC Roots的對象包括下面幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表象)中引用的對。
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
再談引用
- 強(qiáng)引用:new出來的對象是強(qiáng)引用雀鹃,只要強(qiáng)引用還存在幻工,垃圾收集器永遠(yuǎn)不會回收掉被引用的對象。
- 軟引用:在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前黎茎,將會把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收囊颅。如果這次回收后還是沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出的異常。
- 弱引用:被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾回收發(fā)生之前踢代。當(dāng)垃圾收集器工作時盲憎,無論當(dāng)前內(nèi)存是否足夠,都會回收掉紙杯弱引用關(guān)聯(lián)的對象胳挎。
- 虛引用:它是最弱的一種關(guān)系饼疙,不會對生存時間構(gòu)成任何影響。為一個對象設(shè)置虛引用的唯一目的就是在被垃圾回收時收到一個通知慕爬。
生存還是死亡
真正宣告一個對象死亡宏多,至少要進(jìn)行兩次標(biāo)記過程。
如果對象經(jīng)過可達(dá)性算法分析后澡罚,發(fā)現(xiàn)沒有與GC Roots相連的引用鏈伸但,它會被第一次標(biāo)記和篩選。篩選的條件是:是否有必要執(zhí)行finalize()方法留搔。當(dāng)且僅當(dāng)該對象覆蓋finalize()方法更胖,并且沒有被虛擬機(jī)調(diào)用過,才能稱之為有必要執(zhí)行finalize()方法隔显。
如果有必要執(zhí)行却妨,虛擬機(jī)會把這個對象放在一個名字叫做F-Queue的隊(duì)列中執(zhí)行。但是并不承諾等待finalize()方法運(yùn)行結(jié)束括眠。這是對象逃脫死亡命運(yùn)的最后一次機(jī)會彪标,稍后GC會對這些對象進(jìn)行第二次小規(guī)模標(biāo)記,如果這些對象沒有拯救自己掷豺,那么基本上就被回收了捞烟。
PS:finalize()方法只會被執(zhí)行一次。另外当船,能用finalize()方法實(shí)現(xiàn)的功能用 try-finally或者其他的方式都能做的更好题画,所以針對finalize()方法可以完全不用。
回收方法區(qū)
方法區(qū)的回收主要是兩部分類容:廢棄常量和無用的類德频。
判定為無用的類要滿足以下三個條件:
- 該類所有的實(shí)例都已經(jīng)被回收苍息,也就是Java堆中不存在該類的任何實(shí)例。
- 加載該類的ClassLoader已經(jīng)被回收
- 該類對應(yīng)的java.lang.Class對象沒有任何地方被引用壹置,無法在任何地方通過反射訪問該類的方法竞思。
虛擬機(jī)可以對滿足上述3個條件的無用類進(jìn)行回收,這里僅僅是“可以”钞护,而不是和對象一樣盖喷,不使用了就必然會回收。
垃圾收集算法
標(biāo)記-清除算法
算法分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象患亿,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象传蹈。
主要不足點(diǎn)有兩個:
- 效率問題押逼,標(biāo)記和清除兩個過程的效率都不算高
- 空間問題,標(biāo)記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存隨便惦界,空間碎片太多可能會導(dǎo)致以后在程序運(yùn)行的過程中需要分配較大的對象時挑格,找不到足夠的連續(xù)內(nèi)存而提前觸發(fā)另一次垃圾收集動作。
后續(xù)收集算法都是基于這種思路對其不足進(jìn)行改進(jìn)沾歪。
復(fù)制算法
將可用內(nèi)存按照容量劃分為大小相等的兩塊漂彤,每次只使用其中的一塊。當(dāng)這一塊內(nèi)存用完了灾搏,就將存貨的對象復(fù)制到另外一塊上面挫望,然后把已使用的內(nèi)存空間一次清理掉。
不足點(diǎn):將內(nèi)存縮小為原來的一半為代價狂窑。
這種算法主要是來回收新生代(但不是1:1的比例來劃分內(nèi)存空間)蛀醉。新生代是一塊較大的Eden空間和兩塊較小的Survivor空間沃测,每次使用Eden和其中一個Survivor。當(dāng)GC時,把Eden和Survivor還存活的對象一次性復(fù)制到另一個Survivor空間上亿驾,最后清理掉Eden空間和剛開用過的Survivor空間喳整。
標(biāo)記-整理算法
標(biāo)記過程仍然與“標(biāo)記-清楚”算法一樣洼怔,但是后續(xù)步驟不是直接對可回收的對象進(jìn)行清理涩馆,而是讓所有已存活的對象都向一段移動,然后直接清理掉端便捷以外的內(nèi)存烫沙。
分代收集算法
分代收集算法并沒有新的思想匹层。一般是把Java堆分為新生代和老年代,然后根據(jù)各個年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ㄐ啃睢T谛律猩ぃ看卫占瘯r有大批對象死去,只有少量對象存活煤率,就采用復(fù)制算法仰冠。而老年代因?yàn)閷ο蟠婊盥瘦^高,而且沒有額外的空間對它進(jìn)行分配擔(dān)保蝶糯,就必須使用“標(biāo)記-清除”或者“標(biāo)記-整理”算法。
HotSpot的算法實(shí)現(xiàn)
枚舉根節(jié)點(diǎn)(哪些內(nèi)存需要回收)
可作為GC Roots的節(jié)點(diǎn)主要在全局性的引用(常量貨類靜態(tài)屬性)辆沦,執(zhí)行上下文(棧幀中的本地變量表)昼捍。
可達(dá)性分析對執(zhí)行時間的敏感主要是GC停頓上,GC進(jìn)行時必須停頓所有的Java執(zhí)行線程(Stop The World)肢扯。其實(shí)這里很好理解妒茬,就像媽媽在家里打掃房間,肯定是要求你坐在座位上別動蔚晨,否則一邊打掃房間乍钻,而你到處走動并且時不時的亂丟東西肛循,那么這個打掃房間是永遠(yuǎn)掃不完的。
但是現(xiàn)在很多應(yīng)用方法區(qū)就數(shù)百兆大小银择,如果逐個檢查這里面的引用多糠,那必然會耗費(fèi)非常多的時間。虛擬機(jī)有沒有辦法解決這個問題呢浩考?有的夹孔。在HotSpot的實(shí)現(xiàn)中,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個目的的析孽,在類加載完成的時候搭伤,HotSpot就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來,在JIT編譯過程中袜瞬,也會在特定的位置記錄下棧和寄存器中哪些位置是引用怜俐。這樣,GC在掃描時就可以直接得知這些信息了邓尤。
安全點(diǎn)(什么時候回收)
在OopMap的協(xié)助下佑菩,HotSpot可以快速而且準(zhǔn)確的完成GC Roots的枚舉。但是也會存在一些問題裁赠。OopMap內(nèi)容變化的指令非常多的殿漠,如果每一個指令都去生成對應(yīng)的OopMap,那需要大量的額外空間佩捞,這樣GC的空間成本將會變得很高绞幌。
其實(shí)HotSpot也不會這么笨的,去給每條指令都生成OopMap一忱,前面已經(jīng)提到莲蜘,只是在特定的位置記錄了這些信息,這些位置稱之為安全點(diǎn)(Savepoint),即程序執(zhí)行時并非在所有的地方都能停頓下來GC帘营,只有到達(dá)安全點(diǎn)才能停頓票渠。
這里的停頓有兩種方式:
- 搶先式中斷:在GC發(fā)生時,首先把所有的線程全部中斷芬迄。如果發(fā)現(xiàn)有線程中斷的地方不在安全點(diǎn)上问顷,就恢復(fù)線程,讓它“跑”到安全點(diǎn)上≠魇幔現(xiàn)在機(jī)會沒有虛擬機(jī)采取搶先式中斷了杜窄。
- 主動式中斷:當(dāng)GC需要中斷線程的時候,不直接對線程操作算途,僅僅簡單的設(shè)置一個標(biāo)志塞耕。哥哥線程執(zhí)行時主動去輪詢這個標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為真時嘴瓤,就自己中斷掛起扫外。
安全區(qū)域
如果有些線程是“不執(zhí)行狀態(tài)”(比如線程Sleep莉钙,或者Bolcked),這時候線程是無法相應(yīng)JVM的中斷請求筛谚,“走”到安全點(diǎn)去中斷掛起磁玉。對于這種情況,就需要安全區(qū)域(Safe Region)來解決刻获。
我們可以把Safe Region看作是Safepoint的擴(kuò)展版蜀涨。在這個區(qū)域中的任意地方,開始GC都是安全的蝎毡。
在線程執(zhí)行到Safe Region中的代碼時厚柳,首先標(biāo)識自己已經(jīng)進(jìn)入了Safe Region,當(dāng)這段時間JVM要發(fā)起GC時沐兵,就不管標(biāo)識自己為Safe Region的線程了别垮。當(dāng)線程要離開Safe Region時,它會檢查系統(tǒng)是否完成了枚舉根節(jié)點(diǎn)(或者是GC的整個過程)扎谎,如果完成了碳想,線程就繼續(xù)執(zhí)行。否則就等待直到收到可以離開Safe Region的信號為止毁靶。
垃圾收集器
Serial收集器
新生代胧奔、單線程收集器。采用復(fù)制算法预吆。它的“單線程”的意義并不僅僅說明它只會使用一個CPU或者一條收集線程去完成GC龙填,更重要的是,它在GC時拐叉,必須暫停其他所有的工作線程(Stop The World)岩遗,知道它收集結(jié)束。
ParNew收集器
新生代收集器凤瘦。相當(dāng)于Serial的多線程版本(并行的多線程收集器)宿礁。采用復(fù)制算法。也需要Stop The World蔬芥。
Parallel Scavenge收集器
新生代收集器梆靖,使用多線程和復(fù)制算法,需要Stop The World坝茎,也被稱為“吞吐量優(yōu)先”收集器涤姊。
Parallel Scavenge收集器的目標(biāo)是達(dá)到一個可控制的吞吐量(Throughput)。所謂的吞吐量就是CPU用于運(yùn)行用戶代碼的時間與CPU總消耗時間的比值嗤放。即吞吐量=運(yùn)行用戶代碼時間/(運(yùn)行用戶代碼時間+垃圾收集時間)。
高吞吐量和低停頓對虛擬機(jī)來收是兩個矛盾的事壁酬。停頓時間越短就越適合需要與用戶交互的程序次酌,良好的響應(yīng)速度能提升用戶體驗(yàn)恨课。而高吞吐量則可以搞笑的利用CPU的時間,盡快完成運(yùn)算任何岳服,主要適合在后臺運(yùn)算而不需要太多交互的任務(wù)剂公。
Serial Old收集器
相當(dāng)于Serial收集器的老年代版本,單線程收集器吊宋,需要Stop The World纲辽,財務(wù)“標(biāo)記-整理”算法。
Parallel Old收集器
相當(dāng)于Parallel Scavenge收集器的老年代版本璃搜。使用多線程和“標(biāo)記-整理”算法拖吼。
CMS收集器
Concurrent Mark Sweep收集器是一種獲取最短回收停頓時間為目標(biāo)的老年代收集器,采用“標(biāo)記-清除”算法这吻。整個過程分四個步驟:
- 初始標(biāo)記:需要Stop The World吊档,單線程,僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象(不會遞歸)唾糯。
- 并發(fā)標(biāo)記:不需要Stop The World怠硼,單線程,與用戶線程一起工作移怯,進(jìn)行GC Roots Tracing的過程香璃。
- 重新標(biāo)記:需要Stop The World,多線程工作舟误,目的是為了修正并發(fā)標(biāo)記期間葡秒,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致系統(tǒng)標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄。
- 并發(fā)清理:不需要Stop The World脐帝,單線程同云,清除GC Roots不可達(dá)對象。
但是CMS收集器有一下3個明顯的缺點(diǎn):
- CMS收集器對CPU資源非常敏感堵腹。在并發(fā)階段炸站,它雖然不會Stop The World,但是會因?yàn)檎加昧艘徊糠志€程而導(dǎo)致應(yīng)用程序變慢疚顷,總吞吐量降低旱易。
- CMS收集器無法處理浮動垃圾,可能出現(xiàn)“ConCurrent Mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生腿堤。由于CMS并發(fā)清理階段用戶線程還在繼續(xù)運(yùn)行阀坏,所以自然就會有新的垃圾不斷產(chǎn)生,出現(xiàn)在標(biāo)記過程之后產(chǎn)生的垃圾笆檀,在本次收集中CMS無法處理處理掉他們忌堂,只能等待下一次GC再解決。這部分稱之為“浮動垃圾”酗洒。也是由于垃圾手機(jī)階段用戶線程還要繼續(xù)運(yùn)行士修,所以還需要預(yù)留足夠的內(nèi)存空間給用戶線程使用枷遂,CMS無法像其他收集器那樣等老年代幾乎被填滿再GC。
- CMS采用的是“標(biāo)記-清除”算法棋嘲,所有可能會有大量空間碎片產(chǎn)生酒唉。往往老年代還有很大空間剩余,但是卻沒有足夠大的連續(xù)空間來分配當(dāng)前對象沸移,這時候就不得不提前觸發(fā)一次Full GC痪伦。
G1收集器
Garbage-First收集器。它具有以下特點(diǎn):
- 并行與并發(fā):G1手機(jī)起可以通過并發(fā)的方式讓Java程序繼續(xù)執(zhí)行雹锣。
- 分代收集:分代的概念在G1中仍然得以保留网沾。G1收集器不需要其他收集器那樣互相配合管理整個GC堆,它自身可以用不同的方式去處理新創(chuàng)建的對象和已經(jīng)存活一段時間的對象笆制、熬過多次GC的就對象绅这。
- 空間整合:從整體上看G1收集器是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的,從局部(兩個Region)之間上來看是基于“復(fù)制”算法實(shí)現(xiàn)的在辆。無論如何证薇,這意味著G1收集器不會產(chǎn)生空間碎片。
- 可預(yù)測的停頓:除了追求低停頓匆篓,G1還能簡歷可預(yù)測的停頓時間建模浑度,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在GC上的時間不得超過N秒鸦概。
G1不再像其他收集器那樣使用新生代和老年代箩张,它的內(nèi)存布局余其他的收集器有很大的差別,它將Java堆劃分為多個大小相等的獨(dú)立區(qū)域(Region)窗市,雖然它還保留著新生代和老年代的概念先慷,但是新生代和老年代不再是物理隔離的,它們都是一部分Region的集合咨察。
G1跟蹤各個Region里面的垃圾堆積的價值大小论熙,在后臺維護(hù)一個優(yōu)先列表,每次根據(jù)允許手機(jī)的時間摄狱,優(yōu)先回收價值最大的Region脓诡。
G1收集器中,Region之間的對象引用以及其他收集器中新生代與老年代之間的對象引用媒役,虛擬機(jī)都是使用Remembered Set來避免全堆掃描的祝谚。G1中每個Region都有一個與之對應(yīng)的Remeber Set,當(dāng)Reference類型的數(shù)據(jù)引用的對象處于不同的Region之中(分代的例子就是老年代中的對象引用了新生代的對象)酣衷,那么就會把信息記錄到被引用對象所屬的Region的Remebered Set中交惯。當(dāng)GC時,在GC根節(jié)點(diǎn)的枚舉范圍中加入Remebered Set既可保證不對全堆掃描。
如果不記維護(hù)Remebered Set的操作商玫,G1收集器可以分為以下幾個步驟:
- 初始標(biāo)記:需要Stop The World箕憾,單線程牡借,僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象(不會遞歸)拳昌。
- 并發(fā)標(biāo)記:不需要Stop The World,單線程钠龙,與用戶線程一起工作炬藤,進(jìn)行GC Roots Tracing的過程。
- 最終標(biāo)記:需要Stop The World碴里,多線程工作沈矿,目的是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致系統(tǒng)標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄咬腋。
- 最終篩選:需要 Stop The World(和CMS不同)羹膳,多線程,清除GC Roots不可達(dá)對象根竿。根據(jù)SUN公司透露的信息陵像,原本這個階段也是可以和用戶程序一起并發(fā)執(zhí)行的,但是因?yàn)橹皇腔厥找徊糠諶egion寇壳,GC時間可控制醒颖,而且停頓用戶線程也可以大幅度提高GC效率,所以是需要Stop The World壳炎。
內(nèi)存分配與回收的策略
對象主要分配在新生代的Eden區(qū)上泞歉,如果啟動了本地線程分配緩沖,則按線程優(yōu)先在TLAB上分配匿辩,少數(shù)情況也可能直接分配在老年代中腰耙。分配規(guī)則不是100%固定的,取決于當(dāng)前使用的是哪一種垃圾收集器的組合铲球,還有虛擬機(jī)與內(nèi)存相關(guān)的參數(shù)設(shè)置挺庞。
- 新生代GC(Minor GC):指發(fā)生在新生代的垃圾手機(jī)動作,因?yàn)镴ava對象大多數(shù)都具有朝生夕滅的特性睬辐,所以Minor GC非常頻繁挠阁,一般回收速度也比較快。
- 老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC溯饵,出現(xiàn)了Major GC侵俗,經(jīng)常會伴隨至少一次的Minor GC。Major GC的速度一般會比Minor GC慢10倍以上丰刊。
對象優(yōu)先在Eden分配
大多數(shù)情況下隘谣,對象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒有足夠的空間進(jìn)行分配時,虛擬機(jī)將發(fā)生一次Minor GC寻歧。
大對象直接進(jìn)入老年代
所謂的大對象是指掌栅,需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串以及數(shù)組码泛。大對象對與你及的內(nèi)存分配來說猾封,就是一個壞消息(更壞的消息,就是一群“朝生夕滅”的“短命大對象”噪珊,寫程序時應(yīng)該盡量避免)晌缘。經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時,就提前出發(fā)垃圾收集以獲取足夠的連續(xù)空間來“安置”他們痢站。
長期存貨的對象將進(jìn)入老年代
虛擬機(jī)給每個對象定義一個對象年齡(Age)計數(shù)器磷箕。如果對象在Eden區(qū)出生,并經(jīng)過第一次Minor GC后仍然存貨阵难,并且能夠被Survivor容納岳枷,將被移動到Survivor空間中,并且對象年齡設(shè)為1呜叫。對象在Survivor區(qū)中每“熬過”一次Minor GC空繁,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)15歲)怀偷,就會被晉升到老年代中家厌。
動態(tài)對象年齡判定
如果Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代椎工。
空間分配擔(dān)保
在發(fā)生Minor GC之前饭于,虛擬機(jī)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象的總空間,如果大于维蒙,那么Minor GC可以確保是安全的掰吕。如果不大于,則虛擬機(jī)會查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗颅痊。如果不允許殖熟,這是要進(jìn)行一次Full GC。如果允許斑响,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小菱属,如果大于則進(jìn)行一次冒險的Minor GC。如果小于舰罚,則進(jìn)行一次Full GC纽门。
大部分情況下,還是會將HandlePromotionFailure開關(guān)打開营罢,避免Full GC過于頻繁赏陵。