浅析JVM中常见的垃圾收集算法
從如何判定對象消亡的角度出發(fā), 垃圾收集算法可以劃分為“引用計數(shù)式垃圾收集”(Reference
Counting GC) 和“追蹤式垃圾收集”(Tracing GC) 兩大類, 這兩類也常被稱作“直接垃圾收集”和“間接垃圾收集”。
由于主流Java虛擬機中均未涉及引用計數(shù)式垃圾收集算法,因此,本文所有算法均屬于追蹤式垃圾收集的范疇。
對于JAVA虛擬機來說,不同的垃圾收集器采用了不同的垃圾收集算法。同樣,不同的虛擬機,操作內(nèi)存的方法也各不相同,下面介紹幾種常見垃圾收集算法的思想。
常見GC的含義
- 部分收集( Partial GC) :指目標不是完整收集整個Java堆的垃圾收集, 其中又分為:
- 整堆收集( Full GC):收集整個Java堆和方法區(qū)的垃圾收集。
一、標記-清除算法
該算法分為“標記”和“清除”兩個階段
不足之處:
二、復(fù)制算法(標記-復(fù)制算法)
為了解決標記-清除算法的缺陷,復(fù)制算法就被提了出來。
該算法內(nèi)存按容量劃分為大小相等的兩塊, 每次只使用其中的一塊。
當這一塊的內(nèi)存用完了, 就將還存活著的對象復(fù)制到另外一塊上面, 然后再把已使用過的內(nèi)存空間一次清理掉。
這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況, 只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。
優(yōu)點:
不足之處:
現(xiàn)在的商用Java虛擬機大多都優(yōu)先采用了這種收集算法去回收新生代, IBM公司曾有一項專門研究對新生代“朝生夕滅”的特點做了更量化的詮釋——新生代中的對象有98%熬不過第一輪收集。 因此并不需要按照1∶ 1的比例來劃分新生代的內(nèi)存空間。
內(nèi)存使用縮小的解決辦法:Appel式回收
Appel式回收的具體做法:不是按照1:1的比例來劃分內(nèi)存空間, 而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間, 每次使用Eden和其中一塊Survivor。
當回收時,將Eden和Survivor中還存活著的對象一次性地復(fù)制到另外一塊Survivor空間上, 最后清理掉Eden和剛才用過的Survivor空間。
HotSpot虛擬機默認Eden和Survivor的大小比例是8:1, 也就是每次新生代中可用內(nèi)存空間為整個新生代容量的90%( 80%+10%) , 只有10%的內(nèi)存會被“ 浪費”
但是沒有辦法保證每次回收都只有不多于10%的對象存活, 當Survivor空間不夠用時, 需要依賴其他內(nèi)存( 這里指老年代) 進行分配擔保。
如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象, 這些對象便將通過分配擔保機制直接進入老年代, 這對虛擬機來說就是安全的。
三、標記-整理算法
標記-復(fù)制算法在對象存活率較高時就要進行較多的復(fù)制操作, 效率將會降低。 更關(guān)鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況, 所以在老年代一般不能直接選用這種算法。
為了解決復(fù)制算法的缺陷,充分利用內(nèi)存空間,提出了標記-整理算法。
該算法分為“標記”和“整理”兩個階段
優(yōu)點:
不足之處:
標記-清除算法與標記-整理算法的本質(zhì)差異
前者是一種非移動式的回收算法, 而后者是移動式的。
是否移動回收后的存活對象是一項優(yōu)缺點并存的風險決策:如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區(qū)域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種對象移動操作必須全程暫停用戶應(yīng)用程序才能進行,因此,使用者不得不小心翼翼地權(quán)衡其弊端。
但如果跟標記-清除算法那樣完全不考慮移動和整理存活對象的話,彌散于堆中的存活對象導致的空間碎片化問題就只能依賴更為復(fù)雜的內(nèi)存分配器和內(nèi)存訪問器來解決。 譬如通過“分區(qū)空閑分配鏈表”來解決內(nèi)存分配問題(計算機硬盤存儲大文件就不要求物理連續(xù)的磁盤空間, 能夠在碎片化的硬盤上存儲和訪問就是通過硬盤分區(qū)表實現(xiàn)的)。內(nèi)存的訪問是用戶程序最頻繁的操作,假如在這個環(huán)節(jié)上增加了額外的負擔,勢必會直接影響應(yīng)用程序的吞吐量。
基于以上兩點, 是否移動對象都存在弊端。
- 移動則內(nèi)存回收時會更復(fù)雜,不移動則內(nèi)存分配時會更復(fù)雜。
- 從垃圾收集的停頓時間(延遲) 來看,不移動對象停頓時間會更短,甚至可以不需要停頓。
- 從整個程序的吞吐量來看,移動對象會更劃算。因為,即使不移動對象會使得收集器的效率提升一些,但因內(nèi)存分配和訪問相比垃圾收集頻率要高得多,這部分的耗時增加, 總吞吐量仍然是下降的。 HotSpot虛擬機里面關(guān)注吞吐量的Parallel Scavenge收集器是基于標記-整理算法的, 而關(guān)注延遲的CMS收集器(延遲可控的情況下,盡量提高吞吐量)則是基于標記-清除算法的, 這也從側(cè)面印證這點。
另外,還有一種“和稀泥式”解決方案可以不在內(nèi)存分配和訪問上增加太大額外負擔,做法是讓虛擬機平時多數(shù)時間都采用標記-清除算法,暫時容忍內(nèi)存碎片的存在,直到內(nèi)存空間的碎片化程度已經(jīng)大到影響對象分配時,再采用標記-整理算法收集一次,以獲得規(guī)整的內(nèi)存空間。 前面提到的基于標記-清除算法的CMS收集器面臨空間碎片過多時采用的就是這種處理辦法。
四、分代收集算法
分代收集理論
當前商業(yè)虛擬機的垃圾收集器,大多數(shù)都遵循了“分代收集”的理論進行設(shè)計,它建立在三個分代假說之上:
前兩個假說表明:
收集器應(yīng)該將Java堆劃分出不同的區(qū)域, 然后將回收對象依據(jù)其年齡(年齡即對象熬過垃圾收集過程的次數(shù)) 分配到不同的區(qū)域之中存儲。 因此,在Java堆劃分出不同的區(qū)域之后,垃圾收集器才可以每次只回收其中某一個或者某些部分的區(qū)域。
但是對象不是孤立的, 對象之間會存在跨代引用。
同時,前兩個假說也隱約表明:存在互相引用關(guān)系的兩個對象,是應(yīng)該傾向于同時生存或者同時消亡的。 舉個例子,如果某個新生代對象存在跨代引用, 由于老年代對象難以消亡, 該引用會使得新生代對象在收集時同樣得以存活, 進而在年齡增長之后晉升到老年代中,這時跨代引用也隨即被消除了。
第三個假說表明,這種跨代引用僅占極少數(shù)。
因此,我們就不應(yīng)再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用, 只需在新生代上建立一個全局的數(shù)據(jù)結(jié)構(gòu)(該結(jié)構(gòu)被稱為“記憶集”,Remembered Set),這個結(jié)構(gòu)把老年代劃分成若干小塊,標識出老年代的哪一塊內(nèi)存會存在跨代引用。 此后當發(fā)生Minor GC時,只有包含了跨代引用的小塊內(nèi)存里的對象才會被加入到GC Roots進行掃描。雖然這種方法需要在對象改變引用關(guān)系( 如將自己或者某個屬性賦值)時維護記錄數(shù)據(jù)的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是劃算的。
分代收集算法
分代收集算法的核心思想是根據(jù)對象存活的生命周期將內(nèi)存劃分為若干個不同的區(qū)域。
一般情況下將堆區(qū)劃分為老年代和新生代,老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據(jù)不同代的特點采取最適合的收集算法,根據(jù)各個年代的特點采用最適當?shù)氖占惴ā?/p>
在新生代中, 每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復(fù)制算法, 只需要付出少量存活對象的復(fù)制成本就可以完成收集。
而老年代中,因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用 “標記—清理” 或者 “標記—整理” 算法來進行回收。
值得注意的是,分代收集理論也有其缺陷,最新出現(xiàn)( 或在實驗中) 的幾款垃圾收集器都展現(xiàn)出了面向全區(qū)域收集設(shè)計的思想, 或者可以支持全區(qū)域不分代的收集的工作模式。
總結(jié)
針對年輕代,通常采用標記-復(fù)制算法。針對老年代,通常采用標記—清除 或者 標記—整理 算法。在采用在追求高吞吐量的情況下,采用標記—整理 算法,在追求低延遲的情況下,采用標記-清除算法。當前商業(yè)虛擬機的垃圾收集器主要采用分代收集算法。但分代收集理論也有其缺陷,最新出現(xiàn)的幾款垃圾收集器都展現(xiàn)出了面向全區(qū)域收集設(shè)計的思想, 或者可以支持全區(qū)域不分代的收集的工作模式。
總結(jié)
以上是生活随笔為你收集整理的浅析JVM中常见的垃圾收集算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HTTP传输大文件的方法
- 下一篇: 卫星轨道总结