垃圾收集策略與算法
程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧隨線程而生,也隨線程而滅;棧幀隨著方法的開始而入棧,隨著方法的結(jié)束而出棧。這幾個(gè)區(qū)域的內(nèi)存分配和回收都具有確定性,在這幾個(gè)區(qū)域內(nèi)不需要過(guò)多考慮回收的問(wèn)題,因?yàn)榉椒ńY(jié)束或者線程結(jié)束時(shí),內(nèi)存自然就跟隨著回收了。
而對(duì)于 JAVA 堆和方法區(qū),我們只有在程序運(yùn)行期間才能知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,垃圾收集器所關(guān)注的正是這部分內(nèi)存。
判定對(duì)象是否存活
若一個(gè)對(duì)象不被任何對(duì)象或變量引用,那么它就是無(wú)效對(duì)象,需要被回收。
一 引用計(jì)數(shù)法(無(wú)法回收循環(huán)引用的對(duì)象)
在對(duì)象頭維護(hù)著一個(gè) counter 計(jì)數(shù)器,對(duì)象被引用一次則計(jì)數(shù)器 +1;若引用失效則計(jì)數(shù)器 -1。當(dāng)計(jì)數(shù)器為 0 時(shí),就認(rèn)為該對(duì)象無(wú)效了。
引用計(jì)數(shù)算法的實(shí)現(xiàn)簡(jiǎn)單,判定效率也很高,在大部分情況下它都是一個(gè)不錯(cuò)的算法。但是主流的 Java 虛擬機(jī)里沒有選用引用計(jì)數(shù)算法來(lái)管理內(nèi)存,主要是因?yàn)樗茈y解決對(duì)象之間循環(huán)引用的問(wèn)題。
例子:對(duì)象 objA 和 objB 都有字段 instance,令 objA.instance = objB 并且 objB.instance = objA,由于它們互相引用著對(duì)方,導(dǎo)致它們的引用計(jì)數(shù)都不為 0,于是引用計(jì)數(shù)算法無(wú)法通知 GC 收集器回收它們。
二 可達(dá)性分析法(更好)
所有和 GC Roots 直接或間接關(guān)聯(lián)的對(duì)象都是有效對(duì)象,和 GC Roots 沒有關(guān)聯(lián)的對(duì)象就是無(wú)效對(duì)象。
GC Roots 是指:
- Java 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
- 本地方法棧中引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
- GC Roots 并不包括堆中對(duì)象所引用的對(duì)象,這樣就不會(huì)有循環(huán)引用的問(wèn)題。
引用的種類
判定對(duì)象是否存活與“引用”有關(guān)。在 JDK 1.2 以前,Java 中的引用定義很傳統(tǒng),一個(gè)對(duì)象只有被引用或者沒有被引用兩種狀態(tài),我們希望能描述這一類對(duì)象:當(dāng)內(nèi)存空間還足夠時(shí),則保留在內(nèi)存中;如果內(nèi)存空間在進(jìn)行垃圾手收集后還是非常緊張,則可以拋棄這些對(duì)象。很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場(chǎng)景。

在 JDK 1.2 之后,Java 對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為了以下四種。不同的引用類型,主要體現(xiàn)的是對(duì)象不同的可達(dá)性狀態(tài)reachable和垃圾收集的影響。
強(qiáng)引用(Strong Reference)
類似 "Object obj = new Object()" 這類的引用,就是強(qiáng)引用,只要強(qiáng)引用存在,垃圾收集器永遠(yuǎn)不會(huì)回收被引用的對(duì)象。但是,如果我們錯(cuò)誤地保持了強(qiáng)引用,比如:賦值給了 static 變量,那么對(duì)象在很長(zhǎng)一段時(shí)間內(nèi)不會(huì)被回收,會(huì)產(chǎn)生內(nèi)存泄漏。
軟引用(Soft Reference)
軟引用是一種相對(duì)強(qiáng)引用弱化一些的引用,可以讓對(duì)象豁免一些垃圾收集,只有當(dāng) JVM 認(rèn)為內(nèi)存不足時(shí),才會(huì)去試圖回收軟引用指向的對(duì)象。JVM 會(huì)確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對(duì)象。軟引用通常用來(lái)實(shí)現(xiàn)內(nèi)存敏感的緩存,如果還有空閑內(nèi)存,就可以暫時(shí)保留緩存,當(dāng)內(nèi)存不足時(shí)清理掉,這樣就保證了使用緩存的同時(shí),不會(huì)耗盡內(nèi)存。
弱引用(Weak Reference)
弱引用的強(qiáng)度比軟引用更弱一些。當(dāng) JVM 進(jìn)行垃圾回收時(shí),無(wú)論內(nèi)存是否充足,都會(huì)回收只被弱引用關(guān)聯(lián)的對(duì)象。
虛引用(Phantom Reference)
虛引用也稱幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響。它僅僅是提供了一種確保對(duì)象被 finalize 以后,做某些事情的機(jī)制,比如,通常用來(lái)做所謂的 Post-Mortem 清理機(jī)制。

回收堆中無(wú)效對(duì)象
對(duì)于可達(dá)性分析中不可達(dá)的對(duì)象,也并不是沒有存活的可能。
判定 finalize() 是否有必要執(zhí)行
JVM 會(huì)判斷此對(duì)象是否有必要執(zhí)行 finalize() 方法,如果對(duì)象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經(jīng)被虛擬機(jī)調(diào)用過(guò),那么視為“沒有必要執(zhí)行”。那么對(duì)象基本上就真的被回收了。
如果對(duì)象被判定為有必要執(zhí)行 finalize() 方法,那么對(duì)象會(huì)被放入一個(gè) F-Queue 隊(duì)列中,虛擬機(jī)會(huì)以較低的優(yōu)先級(jí)執(zhí)行這些 finalize()方法,但不會(huì)確保所有的 finalize() 方法都會(huì)執(zhí)行結(jié)束。如果 finalize() 方法出現(xiàn)耗時(shí)操作,虛擬機(jī)就直接停止指向該方法,將對(duì)象清除。
對(duì)象重生或死亡
如果在執(zhí)行 finalize() 方法時(shí),將 this 賦給了某一個(gè)引用,那么該對(duì)象就重生了。如果沒有,那么就會(huì)被垃圾收集器清除。
任何一個(gè)對(duì)象的 finalize() 方法只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,如果對(duì)象面臨下一次回收,它的 finalize() 方法不會(huì)被再次執(zhí)行,想繼續(xù)在 finalize() 中自救就失效了。
回收方法區(qū)內(nèi)存
方法區(qū)中存放生命周期較長(zhǎng)的類信息、常量、靜態(tài)變量,每次垃圾收集只有少量的垃圾被清除。方法區(qū)中主要清除兩種垃圾:
- 廢棄常量
- 無(wú)用的類
判定廢棄常量
只要常量池中的常量不被任何變量或?qū)ο笠茫敲催@些常量就會(huì)被清除掉。比如,一個(gè)字符串 "bingo" 進(jìn)入了常量池,但是當(dāng)前系統(tǒng)沒有任何一個(gè) String 對(duì)象引用常量池中的 "bingo" 常量,也沒有其它地方引用這個(gè)字面量,必要的話,"bingo"常量會(huì)被清理出常量池。
判定無(wú)用的類
判定一個(gè)類是否是“無(wú)用的類”,條件較為苛刻。
- 該類的所有對(duì)象都已經(jīng)被清除
- 加載該類的 ClassLoader 已經(jīng)被回收
- 該類的 java.lang.Class 對(duì)象沒有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類的方法。
一個(gè)類被虛擬機(jī)加載進(jìn)方法區(qū),那么在堆中就會(huì)有一個(gè)代表該類的對(duì)象:java.lang.Class。這個(gè)對(duì)象在類被加載進(jìn)方法區(qū)時(shí)創(chuàng)建,在方法區(qū)該類被刪除時(shí)清除。

垃圾收集算法
學(xué)會(huì)了如何判定無(wú)效對(duì)象、無(wú)用類、廢棄常量之后,剩余工作就是回收這些垃圾。常見的垃圾收集算法有以下幾個(gè):
標(biāo)記-清除算法
標(biāo)記的過(guò)程是:遍歷所有的 GC Roots,然后將所有 GC Roots 可達(dá)的對(duì)象標(biāo)記為存活的對(duì)象。
清除的過(guò)程:將遍歷堆中所有的對(duì)象,將沒有標(biāo)記的對(duì)象全部清除掉。與此同時(shí),清除那些被標(biāo)記過(guò)的對(duì)象的標(biāo)記,以便下次的垃圾回收。
這種方法有兩個(gè)不足:
- 效率問(wèn)題:標(biāo)記和清除兩個(gè)過(guò)程的效率都不高。
- 空間問(wèn)題:標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,碎片太多可能導(dǎo)致以后需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
復(fù)制算法(新生代)
為了解決效率問(wèn)題,“復(fù)制”收集算法出現(xiàn)了。它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊內(nèi)存用完,需要進(jìn)行垃圾收集時(shí),就將存活者的對(duì)象復(fù)制到另一塊上面,然后將第一塊內(nèi)存全部清除。這種算法有優(yōu)有劣:
- 優(yōu)點(diǎn):不會(huì)有內(nèi)存碎片的問(wèn)題。
- 缺點(diǎn):內(nèi)存縮小為原來(lái)的一半,浪費(fèi)空間。
- 為了解決空間利用率問(wèn)題,可以將內(nèi)存分為三塊: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一塊 Survivor。回收時(shí),將 Eden 和 Survivor 中還存活的對(duì)象一次性復(fù)制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才使用的 Survivor 空間。這樣只有 10% 的內(nèi)存被浪費(fèi)。
但是我們無(wú)法保證每次回收都只有不多于 10% 的對(duì)象存活,當(dāng) Survivor 空間不夠,需要依賴其他內(nèi)存(指老年代)進(jìn)行分配擔(dān)保。
分配擔(dān)保
為對(duì)象分配內(nèi)存空間時(shí),如果 Eden+Survivor 中空閑區(qū)域無(wú)法裝下該對(duì)象,會(huì)觸發(fā) MinorGC 進(jìn)行垃圾收集。但如果 Minor GC 過(guò)后依然有超過(guò) 10% 的對(duì)象存活,這樣存活的對(duì)象直接通過(guò)分配擔(dān)保機(jī)制進(jìn)入老年代,然后再將新對(duì)象存入 Eden 區(qū)。
標(biāo)記-整理算法(老年代)
標(biāo)記:它的第一個(gè)階段與標(biāo)記/清除算法是一模一樣的,均是遍歷 GC Roots,然后將存活的對(duì)象標(biāo)記。
整理:移動(dòng)所有存活的對(duì)象,且按照內(nèi)存地址次序依次排列,然后將末端內(nèi)存地址以后的內(nèi)存全部回收。因此,第二階段才稱為整理階段。
這是一種老年代的垃圾收集算法。老年代的對(duì)象一般壽命比較長(zhǎng),因此每次垃圾回收會(huì)有大量對(duì)象存活,如果采用復(fù)制算法,每次需要復(fù)制大量存活的對(duì)象,效率很低。
分代收集算法
根據(jù)對(duì)象存活周期的不同,將內(nèi)存劃分為幾塊。一般是把 Java 堆分為新生代和老年代,針對(duì)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>
新生代:復(fù)制算法
老年代:標(biāo)記-清除算法、標(biāo)記-整理算法