所謂垃圾收集泞辐,就是清理已經(jīng)不再使用的內(nèi)存空間硝逢,提高內(nèi)存的利用率秒啦。
由于程序計數(shù)器遭赂、虛擬機棧、本地方法棧都隨線程而生而滅梗劫,棧中的內(nèi)存空間也都基本在編譯期間就可以確定玻淑,所以不需要進行垃圾收集舰涌;而方法區(qū)和Java 堆則不一樣陵像,它們具有不確定性,只有在程序運行期間才能確定會創(chuàng)建哪些對象寇壳,這部分內(nèi)存的回收和分配都是動態(tài)的醒颖,本篇文章后續(xù)講述的“垃圾收集”就是針對這部分內(nèi)存。
談到垃圾收集壳炎,就需要問三個問題:
- What: 哪些內(nèi)存需要回收泞歉?
- When: 什么時候回收逼侦?
- How: 如何回收?
讓我們從這三個基本問題開始進入本篇的學(xué)習(xí)吧腰耙!
哪些內(nèi)存需要回收榛丢?
需要回收的內(nèi)存肯定是沒有再被引用的對象實例,它們永遠(yuǎn)都無法再被繼續(xù)調(diào)用了挺庞,也就是“名存實亡”了晰赞。那么該如何知道對象是否被引用了呢?
引用計數(shù)算法
這個算法很簡單高效选侨,就是一個對象引用它的時候計數(shù)器就加1掖鱼,引用失效(例如A = null
)就減1,當(dāng)一個對象的引用計數(shù)器為0時就說明沒有對象引用它了援制,那就表示它“死亡”了戏挡。
然而至少 Java 虛擬機里并沒有用引用計數(shù)算法進行內(nèi)存管理,因為它無法解決相互循環(huán)引用的問題晨仑。
public class ReferenceCountingGC {
public Object instance = null;
public static void testGC() {
ReferenceCountingGC refA = new ReferenceCountingGC();
ReferenceCountingGC refB = new ReferenceCountingGC();
refA.instance = refB;
refB.instance = refA;
refA = null;
refB = null;
System.gc();
}
}
例如上面一段代碼褐墅,兩個對象分別把持著對方對象的引用,這將導(dǎo)致它們的引用計數(shù)器都不能變?yōu)?洪己,從而也不能被回收妥凳。
可達(dá)性分析算法
在主流的商用程序設(shè)計語言中都基本上采用可達(dá)性分析算法來判斷對象是否存活的。這個算法就是把一系列的GC Root
作為根節(jié)點码泛,從它們開始向下遍歷猾封,若一個對象到任何GC Root
間均不可達(dá),那它就是不可用的對象噪珊,可以被回收晌缘,因此這種算法又叫做根節(jié)點枚舉。
GC Root
卻是不可達(dá)的阵难,所以會被判定為可回收對象岳枷。在 Java 語言中,可作為
GC Root
的對象包括下面幾種:
- 虛擬機棧中引用的對象(也就是局部變量引用的對象)
- 方法區(qū)的靜態(tài)變量所引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧 JNI 引用的對象
就拿上面那段代碼來說呜叫,在執(zhí)行完
refA = null;
refB = null;
以后空繁,那兩個實例對象雖然都懷著對方的引用,但均無法作為GC Root
朱庆,又沒有其他GC Root
與他們建立聯(lián)系盛泡,所以都會被視為可以回收的對象而被回收。
生存還是死亡
然而通過可達(dá)性分析被標(biāo)記為可以回收的對象就一定“非死不可”嗎娱颊?不一定傲诵,要真正宣告一個對象死亡凯砍,至少要經(jīng)歷兩次標(biāo)記過程:在對象被可達(dá)性分析算法標(biāo)記為可以回收后,會進行一次篩選拴竹,篩選出有必要執(zhí)行finalize
方法的對象悟衩,然后把它們放置在一個低優(yōu)先級的線程隊列中依次執(zhí)行finalize
方法。
那什么叫沒有必要執(zhí)行 finalize
方法呢栓拜?就是當(dāng)對象沒有覆蓋默認(rèn)的finalize
方法或者這個finalize
方法之前被虛擬機調(diào)用過一次座泳。這兩種情況下虛擬機都會認(rèn)為沒有必要執(zhí)行finalize
方法,這樣對象就真的死亡了菱属。
而在對象執(zhí)行完finalize
方法之后钳榨,虛擬機會對這些對象進行第二次標(biāo)記,若它還沒有與任何GC Root
建立聯(lián)系的話纽门,那就真的會被回收薛耻。也就是說,只要覆蓋對象的finalize
方法赏陵,使它與任何一個局部變量或靜態(tài)變量建立引用關(guān)系饼齿,那么它就可以免除被回收的命運,從而“自救成功”蝙搔。但這種自救只能拯救一次缕溉,因為一個對象的finalize
方法最多只能被執(zhí)行一次。
什么時候回收吃型?
在進行可達(dá)性分析的時候证鸥,所有線程都需要暫停下來,因為若是出現(xiàn)分析過程中對象的引用關(guān)系還在不斷變化的狀況勤晚,分析結(jié)果的準(zhǔn)確性就無法達(dá)成保障枉层。那么問題就來了,系統(tǒng)應(yīng)該在什么時候暫停所有的線程并進行 GC 呢赐写?
安全點
Java 程序執(zhí)行時并非在任何地方都能停下來開始 GC鸟蜡,只有在到達(dá)一些特定的地方才行,這些地方叫安全點(Safepoint)挺邀。 安全點的選擇既不能太少以致于長時間沒有進行 GC揉忘,又不能太頻繁以致于過分增大運行時的負(fù)荷。所以安全點的選擇基本上是以附近的語句“是否具有讓程序長時間執(zhí)行的特征”為標(biāo)準(zhǔn)進行選定的端铛,而“長時間執(zhí)行”執(zhí)行最明顯的特征就是指令序列復(fù)用泣矛,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)禾蚕、異常跳轉(zhuǎn)等地方附近都會有安全點乳蓄。
對于安全點,還有一個問題需要考慮夕膀,如何在 GC 發(fā)生時讓所有的線程都跑到最近的安全點上停下來虚倒。主要有兩種方案可供選擇:
搶先式中斷:GC 發(fā)生時,先將所有線程都暫停下來产舞,若是有些線程不在安全點魂奥,則恢復(fù)線程,讓它跑到最近的安全點再停下來易猫。這種方案幾乎沒有虛擬機采用耻煤。
主動式中斷:GC 發(fā)生時,系統(tǒng)先將一個標(biāo)志位設(shè)置為真准颓,各個線程在執(zhí)行到安全點的時候判斷這個標(biāo)志位哈蝇,若發(fā)現(xiàn)其為真就中斷掛起。直到所有線程都停下來之后再進行可達(dá)性分析等操作攘已。這個方法簡單高效炮赦,使用比較普遍。
安全區(qū)域
上面的方法似乎完美解決了這個問題样勃,但只是對于正在執(zhí)行的線程吠勘,若是那些處于睡眠或阻塞狀態(tài)的線程,它們顯然無法到達(dá)安全點峡眶,而 JVM 也不可能等待 CPU 重新調(diào)度它剧防。這個時候就需要安全區(qū)域(Safe Region)了。
安全區(qū)域是指在一段引用關(guān)系不會發(fā)生變化的代碼,在這個區(qū)域中的任何時候開始 GC 都是安全的辫樱。安全區(qū)域可以看作是安全點的拓展峭拘。
當(dāng)代碼執(zhí)行到安全區(qū)域時,首先標(biāo)識自己已經(jīng)進入了安全區(qū)域狮暑,那樣如果在這段時間里JVM發(fā)起GC鸡挠,就不用管標(biāo)示自己在安全區(qū)域的那些線程了,在線程離開安全區(qū)域時心例,會檢查系統(tǒng)是否正在執(zhí)行GC宵凌,如果是,就等到GC完成后再離開安全區(qū)域止后。
如何回收瞎惫?
主要的垃圾收集算法有下面三種:
標(biāo)記 - 清除算法
如同它的名字一樣,它只有兩個步驟译株,先是按照可達(dá)性算法標(biāo)記哪些內(nèi)存需要回收瓜喇,然后再一次性清理掉這些內(nèi)存。缺點也很明顯:一是效率問題歉糜,標(biāo)記和清除這兩個過程的效率都比較低乘寒;二是這種算法會產(chǎn)生很多內(nèi)存碎片。
復(fù)制算法
這種算法的核心是“復(fù)制”匪补,先把內(nèi)存分成大小相等的兩部分伞辛,每次都只使用其中的一部分烂翰,當(dāng)使用的那一部分需要內(nèi)存回收的時候,就將還存活著的對象復(fù)制到另一半上蚤氏,然后再將先前的那一半一次清理掉甘耿。這種方法簡單高效,解決了內(nèi)存碎片的問題竿滨,但缺點是每次都只使用總內(nèi)存的一半佳恬,浪費比較大。
然而現(xiàn)在流行的商業(yè)虛擬機都是采用這種算法來回收新生代于游,不過不是將內(nèi)存空間1:1等分毁葱,而是將內(nèi)存分成一塊大的 Eden 分區(qū)和兩塊小的 Survivor 分區(qū),大小比例為8:1贰剥,每次使用 Eden 和其中的一塊 Survivor 分區(qū)倾剿。當(dāng)回收的時候,將 Eden 和 Survivor 中還存活的對象一次性復(fù)制到剩下的 Eden 分區(qū)上鸠澈。
類似下圖所示:
為何可以這樣做呢柱告?IBM公司專門做過研究,新生代中的對象 98% 都是“朝生暮死的”笑陈,也就是說只有很少一部分的對象會在一次 GC 中存活下來际度,所以只占 10% 的 Survivor分區(qū)也是基本夠用的。不過涵妥,98% 的對象可回收只是一般情況下乖菱,無法保證每次存活的對象都只有不到 10%,那么就需要分配擔(dān)保了蓬网,意思就是當(dāng) Survivor 分區(qū)內(nèi)沒有足夠的空間存放上一次從新生代存活的對象的時候窒所,多出的這部分對象會直接進入老年代區(qū)域。
標(biāo)記 - 整理算法
由于復(fù)制算法需要花很多時間進行存活對象的復(fù)制帆锋,效率比較低吵取,而且還需要分配擔(dān)保。而對于老年代區(qū)域的對象锯厢,沒有額外的區(qū)域進行分配擔(dān)保皮官,就無法使用復(fù)制算法,于是“標(biāo)記 - 整理”算法應(yīng)運而生实辑。
該算法的標(biāo)記過程與“標(biāo)記 - 清除”算法一樣捺氢,但后續(xù)步驟不是對可回收對象進行回收,而是讓存活的對象都向同一端移動剪撬,然后清理掉端邊界以外的內(nèi)存摄乒,如圖所示:分代收集算法
主流的商業(yè)虛擬機都采用“分代收集算法”,就是將內(nèi)存區(qū)域根據(jù)對象存活周期將內(nèi)存分成幾塊。具體是分成新生代和老年代馍佑,再根據(jù)不同的年代使用適合它們的垃圾收集算法斋否。
例如新生代中,每次垃圾收集會有大量的對象死去挤茄,存活的較少如叼,故可以采用復(fù)制算法。
而老年代中穷劈,對象存活率高、沒有額外的空間進行分配擔(dān)保踊沸,故可以使用“標(biāo)記 - 清除”或“標(biāo)記 - 整理”算法歇终。
總結(jié)
本篇文章介紹了垃圾收集的三個核心部分:垃圾收集的對象的判定,垃圾收集的時間和垃圾收集的方法逼龟。下一節(jié)將介紹 HotSpot 虛擬機中幾種具體的垃圾收集器是如何實現(xiàn)的评凝。