一苏潜、概述
在之前介紹了Java
內(nèi)存運(yùn)行時區(qū)域的各個部分银萍,其中程序計數(shù)器、虛擬機(jī)棧恤左、本地方法棧三個區(qū)域和線程的生命周期一致贴唇。棧中的棧幀隨著方法的將納入和退出有條不紊的執(zhí)行著出棧和入棧操作。每一個棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時就已知的(盡管在運(yùn)行期會由JIT
編譯器進(jìn)行一些優(yōu)化飞袋,但此處不考慮)戳气,因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個區(qū)域內(nèi)就不需要過多考慮回收的問題巧鸭,因為方法結(jié)束或線程結(jié)束時物咳,內(nèi)存自然就跟著回收了。而Java
堆和方法區(qū)則不一樣蹄皱,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣览闰,一個方法中的多個分支需要的內(nèi)存也可能不一樣,只有在程序處于運(yùn)行期間才能知道會創(chuàng)建哪些對象巷折,這部分內(nèi)存的分配和回收都是動態(tài)的压鉴,垃圾收集器所關(guān)注的是這部分內(nèi)存。
二锻拘、對象已死嗎
在堆里存放著Java
世界中幾乎所有的對象實例油吭,垃圾收集器在對堆進(jìn)行回收前,第一件事情就是要確定這些對象之中哪些還“存活”署拟,哪些已經(jīng)“死去”婉宰。
2.1 引用計數(shù)算法
引用計數(shù)算法就是給對象中添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時推穷,計數(shù)器值就加一心包;當(dāng)引用失效時,計數(shù)器就減一馒铃;任何時刻計數(shù)器為零的對象就是不可能再被使用的蟹腾。這種算法在很多地方都用,但是至少主流的Java
虛擬機(jī)里面沒有選用此算法來管理內(nèi)存区宇,其中最主要的原因是它很難解決對象之間相互循環(huán)引用的問題娃殖。
public class ReferenceCountingGC{
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC(){
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//假設(shè)在這行發(fā)出GC, objA和objB是否能被回收议谷?
System.gc();
}
}
說明:互相被引用炉爆,則GC
是不會回收的(若使用引用計數(shù)算法)。
2.2 可達(dá)性分析算法
在主流的商用程序語言的主流實現(xiàn)中,都是稱通過可達(dá)性分析來判斷對象是否存活的芬首。這個算法的基本思路就是通過一些列的稱為“GC Roots”
的對象作為起點(diǎn)赴捞,從這個節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain
)衩辟,當(dāng)一個對象到GC Roots
沒有任何引用鏈相連時螟炫,則證明此對象是不可用的。如圖所示艺晴,對象object5昼钻、object6、object7
雖然互相有關(guān)聯(lián)封寞,但是它們到GC Roots
是不可達(dá)的然评,所以將會被判定為是可回收的對象。
在Java
語言中狈究,可作為GC Roots
的對象包括下面幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中
JNI
(即一般說的Native
方法)引用的對象
2.3 再談引用
無論使用什么方法碗淌,判斷對象是否存活都與“引用”有關(guān)。在JDK 1.2
以前抖锥,Java
中的引用的定義很傳統(tǒng):如果reference
類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址亿眠,就稱這塊內(nèi)存代表著一個引用。這種定義太過狹隘磅废,我們希望能描述這樣一類對象:當(dāng)內(nèi)存空間還足夠時纳像,則能保留在內(nèi)存之中;如果內(nèi)存空間在進(jìn)行垃圾收集后還是非常緊張拯勉,則可以拋棄這些對象竟趾。
在JDK 1.2
之后,Java
對引用的概念進(jìn)行了擴(kuò)充宫峦,將引用分為強(qiáng)引用(Strong Reference
)岔帽、軟引用(Soft Reference
)、弱引用(Weak Reference
)导绷、虛引用(Phantom Reference
)四種犀勒,引用強(qiáng)度依次逐漸減弱。
強(qiáng)引用就是指在程序代碼中普遍存在的诵次,類似
“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)存溢出異常陌宿。在
JDK 1.2
之后锡足,提供了SoftReference
類來實現(xiàn)軟引用。弱引用頁式用來描述非必需對象的壳坪,但是它的強(qiáng)度比軟引用更弱一些舶得,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時爽蝴,無論當(dāng)前內(nèi)存是否足夠沐批,都會回收掉只被弱引用關(guān)聯(lián)的對象。在
JDK 1.2
之后蝎亚,提供了WeakReference
類來實現(xiàn)軟引用九孩。虛引用也稱為幽靈引用或幻影引用,是最弱的一種引用關(guān)系发框。一個對象是否有虛引用的存在躺彬,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例梅惯。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知宪拥。在
JDK 1.2
之后,提供了PhantomReference
類來實現(xiàn)軟引用铣减。
2.4 生存還是死亡
即使在可達(dá)性分析算法中不可達(dá)的對象她君,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段徙歼,要真正宣告一個對象死亡犁河,至少要經(jīng)歷兩次標(biāo)記過程:
如果對象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)有不可達(dá)對象,那它將會被第一次標(biāo)記并且進(jìn)行一次篩選魄梯,篩選的條件是此對象是否有必要執(zhí)行
fanalize()
方法桨螺。當(dāng)對象沒有覆蓋finalize()
方法,或者finalize()
方法已經(jīng)被虛擬機(jī)調(diào)用過酿秸,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”灭翔。如果這個對象被判定為有必要執(zhí)行
finalize()
方法,那么這個對象將會放置在一個叫做F-Queue
的隊列之中辣苏,并在稍后由一個虛擬機(jī)自動建立的肝箱、低優(yōu)先級的Finalizer
線程去執(zhí)行。所謂的執(zhí)行是指虛擬機(jī)會觸發(fā)這個方法稀蟋,但是不保證會等待它運(yùn)行結(jié)束煌张。因為如果對象在finalize()
中執(zhí)行緩慢或發(fā)生死循環(huán),將導(dǎo)致F-Queue
隊列其他對象一直處于等待退客,甚至導(dǎo)致整個內(nèi)存回收系統(tǒng)奔潰骏融。finalize()
方法是對象逃脫死亡命運(yùn)的最后一次機(jī)會链嘀,稍后GC
將對F-Queue
中的對象進(jìn)行第二次小規(guī)模的標(biāo)記,如果對象要在finalize()
中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可档玻,如把自己(this
)賦給某個變量或?qū)ο蟮某蓡T變量怀泊,那在第二次標(biāo)記時它將被移除“即將回收”的集合;如果對象這時候還沒有逃脫误趴,那基本上它就真的被回收了霹琼。一般不鼓勵使用
finalize()
方法來拯救對象,或者建議忘掉此方法凉当。
2.5 回收方法區(qū)
在方法區(qū)中進(jìn)行垃圾收集的“性價比”一般較低:在堆中枣申,尤其在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收
70%~95%
的空間纤怒,而永久代的垃圾收集效率遠(yuǎn)低于此糯而。永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類〔淳剑回收廢棄常量與回收堆中的對象類似熄驼。在發(fā)生內(nèi)存回收時,如果一個常量在任何地方都沒有被引用烘豹,則就會被回收瓜贾,常量池中的其他類(接口)、方法携悯、字段的符號引用也與此類似祭芦。
-
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件在要苛刻許多憔鬼。需要同時滿足是三個條件:
- 該類所有的實例都已經(jīng)被回收龟劲,也就是堆中不存在該類的任何實例
- 加載該類的
ClassLoader
已經(jīng)被回收 - 該類對應(yīng)的
java.lang.Class
對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法轴或。
虛擬機(jī)可以對滿足上述三個條件的無用類進(jìn)行回收昌跌,但并不是和對象一樣,不使用了就必然會回收照雁。是否對類進(jìn)行回收蚕愤,
HotSpot
虛擬機(jī)提供了-Xnoclassgc
參數(shù)進(jìn)行控制,還可以使用-verbose:class
以及-XX:+TraceClassLoading饺蚊、-XX:+TraceClassUnLoading
查看類加載和卸載信息萍诱。
三、垃圾收集算法
3.1 標(biāo)記-清除算法
算法分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象污呼,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象裕坊,它的標(biāo)記過程其實在前一節(jié)中已經(jīng)說明。之所以說它是最基礎(chǔ)的收集算法燕酷,是因為后續(xù)的收集算法都是基于此算法思路并對其不足進(jìn)行改進(jìn)而得到的碍庵。
主要不足有兩個:一個效率問題映企,標(biāo)記和清除兩個過程的效率都不高悟狱;另一個是空間問題静浴,標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對象時挤渐,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾回收動作苹享。此算法的執(zhí)行過程如下:
3.2 復(fù)制算法
為了解決效率問題,一種稱為“復(fù)制”的收集算法出現(xiàn)了浴麻,它將可用內(nèi)存按容量劃分為大小相等的兩塊得问,每次只使用其中一塊。當(dāng)這一塊內(nèi)存用完了软免,就將還存活著的對象復(fù)制到另一塊上面宫纬,然后再把已使用過的內(nèi)存空間一次性清理掉。這樣使得每次都是對整個半?yún)^(qū)進(jìn)行內(nèi)存回收膏萧,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況漓骚,只要移動堆頂指針,按順序分配內(nèi)存即可榛泛,實現(xiàn)簡單蝌蹂,運(yùn)行高效。只是這種算法的代價是將內(nèi)存縮小為原來的一半曹锨,未免太高了孤个,執(zhí)行過程如下:
現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代,IBM
公司研究標(biāo)明沛简,新生代中的對象98%
都是“朝生夕死”的齐鲤,所以并不需要按照1:1
的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden
空間和兩塊較小的Survivor
空間椒楣,每次使用Eden
和其中一塊Servivor
给郊。當(dāng)回收時,將Eden
和Survivor
中還存活的對象一次性復(fù)制到另一塊Survivor
空間上撒顿,最后清理掉Eden
和剛才用過的Survivor
空間丑罪。HotSpot
虛擬機(jī)默認(rèn)Eden
和Survivor
的大小比例是8:1
,也就是每次新生代中可用內(nèi)存空間為整個新生代容量的90%
凤壁,只有10%
會被浪費(fèi)吩屹。當(dāng)然這不是絕對的,沒有辦法保證每次回收都只有不多于10%
的對象存活拧抖,當(dāng)Survivor
空間不夠用時煤搜,需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保。
也就是說唧席,如果另一塊Survivor
空間沒有足夠空間存放上一次新生代收集下來的存活對象擦盾,這些對象將直接通過分配擔(dān)保機(jī)制進(jìn)入老年代嘲驾。
3.3 標(biāo)記—整理算法
復(fù)制收集算法在對象存活率較高時就要進(jìn)行較多的復(fù)制操作,效率將會變低迹卢。而且辽故,如果不想浪費(fèi)50%
的空間,就需要有額外的空間進(jìn)行分配擔(dān)保腐碱,以應(yīng)對被使用的內(nèi)存中所有對象讀100%
存活的極端情況誊垢,所以在老年代一般不能直接選用這種算法。
根據(jù)老年代的特點(diǎn)症见,有人提出了另一種“標(biāo)記-整理”算法喂走,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理谋作,而是讓所有存活的對象都向一端移動芋肠,然后直接清理掉端邊界以外的內(nèi)存。如圖所示遵蚜。
3.4 分代收集算法
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”算法帖池。這種算法并沒有什么新的思想,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊谬晕。一般是把Java堆分為新生代和老年代碘裕,這樣就可以根據(jù)各個年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律性芮看卫占瘯r都發(fā)現(xiàn)有大批對象死去帮孔,那就選用復(fù)制算法。而老年代中因為對象存活率高不撑、沒有額外空間對它們進(jìn)行分配擔(dān)保文兢,就必須使用“標(biāo)記-清理”或者“標(biāo)記-整理”算法來進(jìn)行回收。