Java 使用了垃圾收集器來代替手動(dòng)管理內(nèi)存,對于垃圾收集器來說扯躺,無論哪種,其核心思想都是做兩件事:
- 找到哪些對象是存活的(還在使用)
- 清除死掉的(不再使用)的對象
標(biāo)記存活對象:
引用計(jì)數(shù)法
最直接秋冰,最容易想到的標(biāo)記方法是引用計(jì)數(shù)法犁罩,顧明思議,記錄每個(gè)對象被引用的個(gè)數(shù)产弹,如果為0派歌,則為死亡對象。該方法實(shí)現(xiàn)簡單痰哨,判斷效率高胶果,但很難解決對象之間相互循環(huán)引用的問題。
可達(dá)性分析計(jì)算
在JVM中使用了可達(dá)性分析計(jì)算的方式來標(biāo)記存活對象斤斧,GC 定義了一些特殊的對象作為 GC Roots:
- 棧幀中的本地變量和參數(shù)
- 活躍線程
- 已加載的靜態(tài)變量
- JNI 引用
以 GC Roots 作為起始點(diǎn)早抠,沿著引用路徑不斷搜索,同時(shí)標(biāo)記搜索到的對象為存活撬讽。
注意蕊连,在標(biāo)記階段,需要停止應(yīng)用線程(Stop the World)游昼,因?yàn)闆]有辦法在應(yīng)用程序不斷改變引用關(guān)系的同時(shí)一邊標(biāo)記甘苍。暫停的時(shí)間取決于存活對象的多少,存活的對象越多烘豌,需要標(biāo)記的時(shí)間越長羊赵。
清除死亡對象
Sweep and Compact
在學(xué)術(shù)上,標(biāo)記清除算法是最具代表性的算法:
Marking: 通過 GC Roots 開始搜索標(biāo)記可達(dá)的對象
Sweeping: 使得被未標(biāo)記的對象占用的內(nèi)存空間可以被之后分配使用
但是這樣直接 Sweep 會(huì)存在兩個(gè)問題:
- 寫操作時(shí)需要尋找可用塊扇谣,會(huì)更加費(fèi)時(shí)
- 空間碎片太多會(huì)導(dǎo)致分配較大對象時(shí)無法得到足夠連續(xù)內(nèi)存
為了避免這個(gè)問題昧捷,還需要做一次碎片整理
Copy
還有一種更簡單的方法,將內(nèi)存分為兩塊罐寨,每次只使用其中的一塊區(qū)域靡挥,發(fā)生 GC 時(shí),將存活的對象復(fù)制到另一個(gè)區(qū)域中鸯绿,這樣也不會(huì)出現(xiàn)碎片的問題跋破,此外簸淀,復(fù)制可以在標(biāo)記的同時(shí)進(jìn)行,更加高效毒返。缺點(diǎn)也很明顯租幕,需要更多的內(nèi)存。這種算法被稱為標(biāo)記-復(fù)制算法拧簸。
分代假說
研究人員觀察到劲绪,應(yīng)用程序內(nèi)的大多數(shù)分配分為兩類:
- 大部分對象創(chuàng)建后很快就不再使用
- 有一些對象會(huì)存活很長一段時(shí)間
基于這個(gè)假設(shè),虛擬機(jī)將內(nèi)存分為了兩個(gè)代盆赤,分別為 新生代(Young Generation)贾富, 和老年代(Old Generation or Tenured)。
那么針對不同代的特點(diǎn)牺六,可以有針對性的進(jìn)行算法優(yōu)化颤枪,一般來說將算法分為 Mionr GC(只回收新生代對象)和 Full GC(全局回收)。
這個(gè)假設(shè)也存在兩個(gè)問題:
- 兩個(gè)代之間的對象可能存在引用淑际,即使只進(jìn)行 Mionr gc畏纲,也需要掃描一遍老年代對象檢查是否存在老年代對象引用新生代對象,違背了分代的初衷
- 分代假設(shè)可能不適用于某些應(yīng)用春缕。由于GC對算法是專門針對快速死亡的對象和存活長時(shí)間的對象進(jìn)行了優(yōu)化霍骄,因此對有“中等”壽命的對象的處理,JVM 表現(xiàn)的不太好
內(nèi)存劃分
通常情況下 Eden 是對象創(chuàng)建時(shí)被分配的區(qū)域淡溯。由于涉及到多個(gè)線程同時(shí)創(chuàng)建對象读整,Eden 被劃分成了一個(gè)或多個(gè) Thread Local Allocation Buffer (TLAB) ,簡單來說咱娶,每個(gè)線程都被分配了一塊區(qū)域用于本線程的對象分配(避免的線程同步代價(jià))米间,如果分配的內(nèi)存不夠使用了,則使用共有的部分(申請新的TLAB)膘侮,如果再不夠屈糊,則觸發(fā)一次新生代 GC(Minor GC) ,如果清理后的內(nèi)存仍然不夠琼了,則將對象分配在老年代逻锐。
在 Mionr GC 時(shí),首先通過 GC Roots 掃描標(biāo)記所有存活的對象雕薪,需要注意之前提到過昧诱,老年代的對象也有可能引用新生代對象。對于這個(gè)問題所袁,JVM 使用了 card-marking 來避免老年代的掃描盏档。HotSpot 使用了卡表(Card Table)的技術(shù),將整個(gè)堆劃分為一個(gè)個(gè)大小為512字節(jié)的卡燥爷,如果卡中的對象可能指向新生代對象引用蜈亩,那么這張卡是臟的懦窘,同時(shí) JVM 維護(hù)了一個(gè)卡表,每張卡都有一個(gè)對應(yīng)的標(biāo)識位來表示是否是臟卡稚配。那么在進(jìn)行 Minor GC 時(shí)畅涂,只需將臟卡中的對象將入到 GC Roots 里,而不用掃描整個(gè)老年代道川。
完成標(biāo)記后午衰,將所有存活的對象復(fù)制到其中一個(gè) Survivor 區(qū)中,此后整個(gè) Eden 區(qū)的內(nèi)存都可以重新被使用了愤惰。這個(gè)算法也叫做“標(biāo)記-復(fù)制“算法(Mark and Copy)。
Survivor 分為 from 和 to 兩個(gè)區(qū)域(每次GC后身份互換)赘理,其中 to 區(qū)域永遠(yuǎn)是空的宦言,當(dāng)GC完成標(biāo)記后,Eden 和 from 區(qū)域的存活對象都復(fù)制到 to 區(qū)域中商模,from區(qū)域清空奠旺,兩個(gè)區(qū)域身份互換。
對象可能在兩個(gè) Survivor 中不斷的來回復(fù)制施流,當(dāng)復(fù)制達(dá)到一定次數(shù)時(shí)(默認(rèn)15次)响疚,將被認(rèn)為足夠老,晉升到老年代中瞪醋。此外忿晕,如果 Survivor 區(qū)域大小不夠存放所有存活對象,則會(huì)將較老的對象提前晉升到老年代中银受。
老年代內(nèi)存要大的多践盼,并且大多數(shù)對象都不會(huì)是垃圾,并且發(fā)生GC的頻率要相對小的多宾巍,所以復(fù)制算法不適用咕幻。一般來說,使用“標(biāo)記-清除-整理“的算法對老年代進(jìn)行回收顶霞。