深入理解JVM(二)--垃圾收集算法
一.?概述
說起垃圾收集(Garbage?Collection,?GC),?大部分人都把這項(xiàng)技術(shù)當(dāng)做Java語(yǔ)言的伴隨生產(chǎn)物.?事實(shí)上,?GC的歷史遠(yuǎn)遠(yuǎn)比Java久遠(yuǎn), 1960年?誕生于MIT的Lisp是第一門真正使用內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)的語(yǔ)言.?當(dāng)Lisp還在胚胎時(shí)期時(shí),人們就在思考GC需要完成的三件事情:?
- 哪些內(nèi)存需要回收?
- 什么時(shí)候回收?
- 如何回收?
現(xiàn)在內(nèi)存的動(dòng)態(tài)分配與內(nèi)存回收技術(shù)已經(jīng)相當(dāng)成熟,?那為什么我們還要去了解GC和內(nèi)存分配呢??答案很簡(jiǎn)單:?當(dāng)需要排查各種內(nèi)存溢出,?內(nèi)存泄漏問題時(shí),?當(dāng)垃圾收集稱為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時(shí),?我們就需要對(duì)這些"自動(dòng)化"的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié).
二.?對(duì)象的生與死
堆中幾乎存放著Java世界中所有的對(duì)象實(shí)例,?垃圾收集器在對(duì)堆進(jìn)行回收前,?第一件事情就是要確定這些對(duì)象還有哪些還"存活",?哪些已經(jīng)"死去"(即不可能再被任何途徑適用的對(duì)象).
- 引用計(jì)數(shù)算法
概念: 給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,?每當(dāng)有一個(gè)地方引用它時(shí),?計(jì)數(shù)器值+1;?當(dāng)引用失效時(shí),?計(jì)數(shù)器值-1;?任何時(shí)刻計(jì)數(shù)器都為0的對(duì)象就是不可能再被使用的.?
客觀地說,?引用計(jì)數(shù)算法(Reference?Counting)?的實(shí)現(xiàn)簡(jiǎn)單,?判定效率也很高,?在大部分情況下它都是一個(gè)不錯(cuò)的算法,?也有一些比較著名的應(yīng)用案例,?例如微軟的COM(Component?Object?Model)?技術(shù),?使用ActionScript 3的FlashPlayer,?Python語(yǔ)言以及在游戲腳本領(lǐng)域被廣泛引用的Squirrel中都使用了引用計(jì)數(shù)算法進(jìn)行內(nèi)存管理.但是, Java語(yǔ)言沒有選用引用計(jì)數(shù)算法來管理內(nèi)存,?其中最主要的原因是它很難解決對(duì)象之間的相互循環(huán)引用的問題.
? 2.?根搜索算法
概念:?通過一系列的名為"GC Roots"?的對(duì)象作為起始點(diǎn),?從這些節(jié)點(diǎn)開始向下搜索,?搜索所走過的路徑稱為引用鏈(Reference?Chain),?當(dāng)一個(gè)對(duì)象到GC?Roots?沒有任何引用鏈想連(用圖論的話來說就是從GC?Roots到這個(gè)對(duì)象不可達(dá))時(shí),?則證明此對(duì)象是不可用的.?
在Java和C#,?以及上面提到的古老的Lisp,?都是使用跟搜索算法(GC?Roots?Tracing)?判斷對(duì)象是否存活的.?
在Java語(yǔ)言里,?可作為GC?Roots的對(duì)象包括下面幾種:?
- 虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象.
- 方法區(qū)中的類靜態(tài)屬性引用的對(duì)象.
- 方法區(qū)中的常量引用的對(duì)象.
- 本地方法棧中JNI(即一般說的Native方法)的引用的對(duì)象.
3.?生存還是死亡
在跟搜索算法中不可達(dá)的對(duì)象,?也并非是"非死不可"的,?這時(shí)候他們暫時(shí)處于"緩刑"階段,?要真正宣告一個(gè)對(duì)象死亡,?至少要經(jīng)歷兩次標(biāo)記過程:?如果對(duì)象在進(jìn)行根搜索后發(fā)現(xiàn)沒有與GC?Roots相連接的引用鏈,?那它將會(huì)第一次被標(biāo)記并且進(jìn)行一次篩選,?篩選的條件是此對(duì)象是否有必要進(jìn)行finalize()方法,?當(dāng)對(duì)象沒有覆蓋finalize()?方法,?或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用郭,?虛擬機(jī)將這兩種情況都視為"沒有必要執(zhí)行".
如果這個(gè)對(duì)象有必要執(zhí)行finalize()方法,?那么這個(gè)對(duì)象將會(huì)被放置在一個(gè)名為F-Queue的隊(duì)列之中,?并在稍后由一條由虛擬機(jī)自動(dòng)建立的,?低優(yōu)先級(jí)的Finalizer線程去執(zhí)行.?finalize()方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì),?稍后GC將對(duì)F-Queue中的對(duì)象進(jìn)行第二次小規(guī)模標(biāo)記,?如果對(duì)象要在finalize()中成功拯救自己---只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,?譬如把自己賦值給某個(gè)類變量或?qū)ο蟮某蓡T變量,?那在第二次標(biāo)記時(shí)它將被移除出"即將回收的集合",?如果對(duì)象這時(shí)候還沒有逃脫,?那它就這的離死不遠(yuǎn)了.?
代碼:?一次對(duì)象自我拯救的演示
public class FinalizeEscapeGC {public static FinalizeEscapeGC SAVE_HOOK = null;public void isAlive() {System.out.println("Yes, I'm still alive.");}@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("Finalize method executed!");FinalizeEscapeGC.SAVE_HOOK = this;}public static void main(String[] args) throws Throwable {SAVE_HOOK = new FinalizeEscapeGC();SAVE_HOOK = null;System.gc();Thread.sleep(500);if (SAVE_HOOK != null) {SAVE_HOOK.isAlive();} else {System.out.println("No, I'm dead.");}SAVE_HOOK = null;System.gc();Thread.sleep(500);if (SAVE_HOOK != null) {SAVE_HOOK.isAlive();} else {System.out.println("No, I'm dead.");}}}
運(yùn)行結(jié)果:?
Finalize method executed!
Yes, I'm still alive.
No, I'm dead. 三.?垃圾收集算法
1.?標(biāo)記-清除算法(Mark-Sweep)
如他的名字一樣,?算法分為"標(biāo)記"和"清除"兩個(gè)階段:?首先標(biāo)記出所有需要回收的對(duì)象,?在標(biāo)記完成后統(tǒng)一會(huì)受到所有被標(biāo)記的對(duì)象,它的標(biāo)記過程在上面講述對(duì)象標(biāo)記判定時(shí)已經(jīng)基本介紹過了.?它是最基礎(chǔ)的收集算法,?是因?yàn)楹罄m(xù)的書記算法都是基于這種思路對(duì)其缺點(diǎn)進(jìn)行改進(jìn)而得到的.?
它的主要缺點(diǎn)有兩個(gè):?一個(gè)是效率問題,?標(biāo)記和清除過程的效率都不高,?另一個(gè)是空間問題,?標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,?空間碎片太多可能會(huì)導(dǎo)致,?當(dāng)程序在以后的運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作.
2.?復(fù)制收集算法(Coping)
為了解決效率問題,?一種稱為"復(fù)制"的收集算法出現(xiàn)了,?它將可用內(nèi)存按容量劃分為大小相等的兩塊,?每次只使用其中的一塊.?當(dāng)著一塊的內(nèi)存用完了,?就將還存活著的對(duì)象復(fù)制到另一塊上面,?然后再把已使用過的內(nèi)存空間一次清理掉,?這樣使得每次都是對(duì)其中的一塊進(jìn)行內(nèi)存回收,?內(nèi)存分配時(shí)也不用考慮內(nèi)存碎片等復(fù)雜情況,?只要移動(dòng)堆頂指針,?按順序分配內(nèi)存即可,?實(shí)現(xiàn)簡(jiǎn)單,?運(yùn)行高效.?只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半,?未免太高了一點(diǎn).
現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來回收新生代,?IBM的專門研究表明,?新生代中的對(duì)象98%都是朝生夕死,?所以并不需要按照1:1的比例來話費(fèi)呢內(nèi)存空間,?二十將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,?每次使用Eden和其中的一塊Survivor.?當(dāng)回收時(shí),?將Eden和Survivor中還存活著的對(duì)象一次性的拷貝到另一塊Survivor空間上,?最后清理掉Eden和剛才用過的Survivor的空間,?HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1,?也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80%+ 10%),?只有10%的內(nèi)存會(huì)被"浪費(fèi)".?當(dāng)然98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),?我們沒有辦法保證每次回收都只有不多余10%的對(duì)象存活,?當(dāng)Survivor空間不夠時(shí),?需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle?Promotion).
3.?標(biāo)記-整理算法(Mark-Compact)
復(fù)制收集算法在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(fù)制操作,?效率將會(huì)變低,?更關(guān)鍵的是,?如果不想浪費(fèi)50%的空間,?就需要有額外的空間進(jìn)行分配擔(dān)保,?以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,?所以在老年代一般不能直接選用這種算法.
根據(jù)老年代的特點(diǎn),?于是提出了另一種"標(biāo)記-整理"(Mark-Compact)算法,?標(biāo)記過程仍與"標(biāo)記-清除"算法一樣,?但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,?而是讓所有存活的對(duì)象都向一端移動(dòng),?然后直接清理掉邊界以外的的內(nèi)存.
4.?分代收集算法
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用"分代收集"(Generation?Collection)算法,?這種算法并沒有什么新思想,?只是根據(jù)對(duì)象的存活周期的不同將內(nèi)存劃分為幾塊.?一般是把Java堆氛圍新生代和老年代,?這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴??在新生代中,?每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,?只有少量存貨,?那就選用復(fù)制算法,?只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集.?而老年代中因?yàn)閷?duì)象存活率高,?沒有額外的空間對(duì)它進(jìn)行分配擔(dān)保,?就必須使用"標(biāo)記-清理"或"標(biāo)記-整理"算法來進(jìn)行回收.?
轉(zhuǎn)載于:https://www.cnblogs.com/vinsen/p/7879674.html
總結(jié)
以上是生活随笔為你收集整理的深入理解JVM(二)--垃圾收集算法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。