JVM--垃圾回收

一、 如何定位垃圾

  • 垃圾回收的核心工作就是回收垃圾泡一,在JVM 的視角來看:垃圾就是無用的對象占用的堆內(nèi)存空間颤殴。那么如何定位垃圾便是垃圾回收的重中之重。

1. 引用計數(shù)算法(reference counting)

  • 算法思想:給對象中添加一個引用計數(shù)器鼻忠,每當有一個地方引用它時涵但,計數(shù)器就加 1;當引用失效時,計數(shù)器值就減 1矮瘟;一旦某個對象的引用計數(shù)器為 0瞳脓,則說明該對象已經(jīng)死亡,便可被回收澈侠。
  • 缺陷:無法處理循環(huán)引用對象劫侧,如下:
    /**
     * testGC() 方法執(zhí)行后,objA 和 objB是否會被GC埋涧?
     * ClassName: ReferenceCountingGC 
     */
    public class ReferenceCountingGC {
        public Object instance = null;
        private static final int _1MB = 1024 * 1024;
        
        private byte[] bigSize = new byte[2 * _1MB];
        
        public static void testGC() {
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();
            objA.instance = objB;
            objB.instance = objA;
            
            objA = null;
            objB = null;
            
            System.gc();
        }
    }

對象 a, b 相互引用板辽,除此之外沒有其他引用指向a 或者 b,在這種情況下棘催,a 和 b 實際已經(jīng)死亡劲弦,但是由于他們的引用計數(shù)器皆不為 0 ,在引用計數(shù)算法的心中醇坝,這兩個對象還活著邑跪。因此,這些循環(huán)引用對象所占據(jù)的空間將不可回收呼猪,從而造成內(nèi)存泄漏画畅。

  • GC Roots: 可以理解為由堆外指向堆內(nèi)的引用,包括(但不限于)如下幾種:
    1. 虛擬機棧(棧幀中的本地變量表)中引用的對象宋距;
    2. 方法區(qū)中類靜態(tài)屬性引用的對象轴踱;
    3. 方法區(qū)中常量引用的對象;
    4. 本地方法棧中JNI(即Native 方法)引用的對象谚赎;
    5. 已啟動且未停止的 Java 線程淫僻。

2. 可達性分析算法

  • 算法思想:以“GC Roots”的對象作為起始點,若無法到達對象 a 或者 b壶唤,則可達性分析便不會將它們加入存活對象合集中雳灵。其中搜索走過的路徑稱為引用鏈(Reference Chain)
  • 缺陷:在多線程環(huán)境下,其他線程可能會更新已經(jīng)訪問過的對象中的引用闸盔,從而造成誤報(將引用設置為 null)或漏報(將引用設置為被訪問的對象)
  1. 誤報只會使JVM 損失了部分垃圾回收的機會悯辙,即當GC標記完成,還未開始回收迎吵,你更新了其中一個引用躲撰,使之指向 null,那么原來的指向?qū)ο蟊究梢员换厥眨ǖ珱]有被GC 標記為可回收击费,只能等待下次標記)茴肥。
  2. 漏報是已經(jīng)被 GC 標記為可回收的對象,更新為被其他對象指向荡灾,垃圾回收器直接給回收掉了瓤狐,則可能會直接導致JVM 崩潰瞬铸。

3. Stop-the-world 以及安全點

  • 目的是為了解決上述標記過程中堆棧的狀態(tài)發(fā)生改變,JVM 采取安全點機制來實現(xiàn) Stop-the-world 操作础锐,暫停其他非垃圾回收線程嗓节。因此老年代回收有卡頓現(xiàn)象。

4. Java的四種引用:強軟弱虛

5. 對象的最后一次自我拯救

  • 即使在可達性算法中不可達的對象皆警,仍有一次自我拯救的機會拦宣。因為宣告一個對象的死亡至少要經(jīng)歷兩次標記過程:對象在可達性算法標記為不可達后進行一次篩選,判斷此對象是否有必要執(zhí)行 finalize() 方法信姓。當對象沒有覆蓋 finalize() 方法鸵隧,或者 finalize() 方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都是為“沒有必要執(zhí)行”意推。
  • 在重寫的 finalize() 方法中豆瘫,只要重新與引用鏈上的任何一個對象建立關聯(lián)即可,如下:
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    
    public void isAlive() {
        System.out.println("yes, i am still alive:)");
    }
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        
        // 對象第一次成功自我拯救
        SAVE_HOOK = null;
        System.gc();
        // 由于 finalize() 方法優(yōu)先級很低菊值,所以暫停 0.5秒等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
        
        // 對象第二次自我拯救 失斖馇!
        SAVE_HOOK = null;
        System.gc();
        // 由于 finalize() 方法優(yōu)先級很低腻窒,所以暫停 0.5秒等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}
運行結(jié)果:
finalize method executed!
yes, i am still alive:)
no, i am dead :(

執(zhí)行結(jié)果一次成功自救昵宇,一次失敗,這是因為任何一個對象的 finalize() 方法都只會被系統(tǒng)自動調(diào)用一次儿子,如果對象面臨下一次回收瓦哎,它的 finalize() 方法不會再次執(zhí)行。

二柔逼、如何回收垃圾

  • 當標記完所有的存活對象是杭煎,便可以對死亡的對象進行回收了

1. 標記 - 清除(Mark-Sweep)

  • 算法思想:把死亡對象所占據(jù)的內(nèi)存標記為空閑內(nèi)存,并記錄在空閑列表(free list)之中卒落。當需要新建對象時,內(nèi)存管理模塊便會從該空閑列表中尋找空閑內(nèi)存蜂桶,并劃分給新建的對象儡毕。


    Sweep
  • 缺點:
    1. 造成內(nèi)存碎片。由于JVM 的堆中對象必須是連續(xù)分布的扑媚,因此可能出現(xiàn)總空閑內(nèi)存足夠腰湾,但是無法分配的極端情況。
    2. 分配效率較低疆股。如果是一塊連續(xù)的內(nèi)存空間费坊,那么可以通過指針加法(pointer bumping)來做分配。而對于空閑列表旬痹,JVM 則需要逐個訪問列表的項附井,來查找能夠放入新建對象的空閑內(nèi)存讨越。

2. 壓縮(compact)也叫標記 - 整理(Mark-Compact)

  • 算法思想:把存活的對象聚集到內(nèi)存區(qū)域的起始位置,從而留下一段連續(xù)的內(nèi)存空間永毅。


    Compact
  • 優(yōu)缺點:能夠解決內(nèi)存碎片化的問題把跨,代價就是壓縮算法的性能開銷。

3. 復制(copy)

  • 算法思想: 把內(nèi)存區(qū)域分為兩等分沼死,分別用兩個指針 from 和 to 來 維護着逐,并且只是用 from 指針指向的內(nèi)存區(qū)域來分配內(nèi)存。當發(fā)生垃圾回收時意蛀,便把存活的對象復制到to 指針指向的內(nèi)存區(qū)域中耸别,并且交換 from 指針 和 to 指針的內(nèi)容。


    Copy
  • 優(yōu)缺點: 能夠解決內(nèi)存碎片化的問題县钥,但堆空間的使用效率極其低下秀姐。

4. 分代收集(Generational Collection)

  • 算法思想:根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般為新生代和 老年代魁蒜,便可根據(jù)各個年代的特點采用最適合的收集算法囊扳。在新生代中,每次垃圾收集時都會有大批對象死去兜看,只有少量存活锥咸,便采用復制算法,而老年代中因為對象存活率高细移、沒有額外空間對它進行分配擔保搏予,則使用 “標記 - 清除” 或者 “標記 -整理” 算法。

5. JVM 的堆劃分

  • 前面提到了新生代 和 老年代弧轧,這是 JVM 對堆的劃分雪侥,其中新生代又被劃分為 Eden 區(qū),以及兩個大小相同的 Survivor 區(qū)。
  • 默認情況下,JVM采取的是一種動態(tài)分配的策略(對應JVM參數(shù) -XX:UsePSAdaptiveSurvivorSizePolicy)螟蝙,根據(jù)生成對象的速率跨细,以及 Survivor 區(qū)使用情況動態(tài)調(diào)整 Eden區(qū) 和 Survivor 卻的比例。
  • 當然,也可以通過參數(shù) -XX:SurvivorRatio 來固定這個比例。不過Survivor區(qū)會一直為空,因此比例越低浪費的堆空間將越高原茅。


    堆空間
  • 當 調(diào)用new 指令時,它會在 Eden 區(qū)中劃出一塊作為存儲對象的內(nèi)存堕仔。由于堆空間是線程共享的擂橘,因此直接在這里劃空間是需要進行同步的。不然可能出現(xiàn)兩個對象公用一段內(nèi)存的事故摩骨。
  • JVM 的解決方法就是:每個線程都可以向JVM 申請一段連續(xù)的內(nèi)存通贞,作為線程私有的TLAB(線程私有緩沖區(qū) Thread Local Allocation Buffer朗若,對應虛擬機參數(shù) -XX:+UseTLAB,默認開啟)滑频。這個操作需要加鎖捡偏,線程需要維護兩個指針(可能更多,主要的就兩個)峡迷,一個指向TLAB 中空余內(nèi)存的起始位置银伟,一個則指向 TLAB 末尾。
  • 接下來的new 指令绘搞,便可以直接通過指針加法(bump the pointer)來實現(xiàn)彤避,即把指向空余內(nèi)存位置的指針加上所請求的字節(jié)數(shù)。如果加法后空余內(nèi)存指針的值仍小于或等于指向末尾的指針夯辖,則代表分配成功琉预。否則,TLAB 已經(jīng)沒有足夠的空間來滿足本次新建操作蒿褂。此時圆米,便需要當前線程重新申請新的 TLAB。
  • 如果 Eden 區(qū)的空間耗盡啄栓,此時JVM 會觸發(fā)一次 Minor GC娄帖,來收集新生代的垃圾。存活下來的對象昙楚,則會被送到 Survivor 區(qū)近速。新生代有兩個 Survivor 區(qū),我們分別用 from 和 to來指代堪旧。其中 to 指向 Survivor 區(qū)是空的削葱。
  • 當發(fā)生 Minor GC時,Eden 區(qū)和 from指向的 Survivor 區(qū)中的存活對象會被復制到 to指向的 Survivor 區(qū)中淳梦,然后交換 from 和 to 指針析砸,以保證下一次 Minor GC時,to 指向的Survivor 區(qū)還是空的爆袍。
  • 新生代和老年代的劃分: JVM會記錄 Survivor 區(qū)中的對象一共被來回復制了幾次首繁。如果一個對象被復制的次數(shù)為 15(對應虛擬機參數(shù) -XX:+MaxTenuringThreshold),那么該對象將被晉升(promote)至老年代螃宙。另外,如果 單個 Survivor 區(qū)已經(jīng)被占用了 50%(對應虛擬機參數(shù) -XX:TargetSurvivorRatio)所坯,那么較高復制次數(shù)的對象也會被晉升至老年代谆扎。
    注:為什么是15 而不是其他?原因是 HotSpot會在對象頭中的標記字段里記錄年齡芹助,分配到的空間只有4位堂湖,因此只能記錄到15
  • 優(yōu)缺點:Minor GC 是不用對整個堆進行垃圾回收闲先,此時有一個問題就是老年代的對象可能引用新生代的對象。也就是說无蜂,在標記存活對象的時候伺糠,我們需要掃描老年代中的對象。如果該對象擁有對新生代對象的引用斥季,你們這個引用也會被作為 GC Roots训桶。如此,豈不是又做了一次全堆掃描酣倾?

6. 卡表(Card Table)

HotSpot為了解決Minor GC的時候不用進行全堆掃描而提供的方案

  • 原理:該技術將整個堆劃分為一個個大小為512 字節(jié)的卡舵揭,并維護一個卡表,用來存儲每張卡的一個標識位躁锡。這個標識位代表對應的卡是否可能存有指向新生代對象的引用午绳。如果存在,你們這張卡就是臟的映之。
  • 在進行 Minor GC的時候拦焚,便可不用掃描整個老年代,而是在卡表中尋找臟卡杠输,并將臟卡中的對象加入到 Minor GC 的 GC Roots中赎败。當完成所有臟卡的掃描后,JVM便可將所有的臟卡的標識位清零抬伺。

三螟够、 垃圾回收器

  • 針對新生代的垃圾回收器有三個:Serial、Parallel Scavenge 和 Parallel New峡钓。都是采用標記 - 復制算法妓笙。其中,Serial 是單線程的能岩,Parallel New 可以看成 Serial 的多線程版本寞宫。Parallel Scavenge 和 Parallel New 類似,但更加注重吞吐量(Throughput)拉鹃,吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)辈赋。此外,Parallel Scavenge 不能與 CMS 一起使用膏燕。
  • 針對老年代的垃圾回收器也有三個:Serial Old 和 Parallel Old钥屈,以及 CMS。Serial Old 和 Paralled Old 都是標記 - 壓縮算法坝辫。前者為單線程的篷就,后者可以看成前者的多線程版本。
  • CMS 采用的是標記 - 清除算法近忙,并發(fā)收集竭业、低停頓智润。但是對CPU敏感、而且會產(chǎn)生空間碎片未辆。
  • G1(Garbage First)是一個橫跨新生代和老年代的垃圾回收器窟绷。它打亂了前面所說的堆結(jié)構(gòu),直接將堆分為極多個區(qū)域咐柜。每個區(qū)域都可以充當 Eden 區(qū)兼蜈、Survivor 區(qū)或者老年代中的一個。采用的是“標記 - 壓縮”算法炕桨,而且和 CMS 一樣都能在應用程序運行過程中并發(fā)的進行垃圾回收饭尝。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市献宫,隨后出現(xiàn)的幾起案子钥平,更是在濱河造成了極大的恐慌,老刑警劉巖姊途,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涉瘾,死亡現(xiàn)場離奇詭異,居然都是意外死亡捷兰,警方通過查閱死者的電腦和手機立叛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贡茅,“玉大人秘蛇,你說我怎么就攤上這事《タ迹” “怎么了赁还?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長驹沿。 經(jīng)常有香客問我艘策,道長,這世上最難降的妖魔是什么渊季? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任朋蔫,我火速辦了婚禮,結(jié)果婚禮上却汉,老公的妹妹穿的比我還像新娘驯妄。我一直安慰自己,他們只是感情好合砂,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布青扔。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赎懦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天幻工,我揣著相機與錄音励两,去河邊找鬼。 笑死囊颅,一個胖子當著我的面吹牛当悔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播踢代,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盲憎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胳挎?” 一聲冷哼從身側(cè)響起饼疙,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎慕爬,沒想到半個月后窑眯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡医窿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年磅甩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姥卢。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡卷要,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出独榴,到底是詐尸還是另有隱情僧叉,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布括眠,位于F島的核電站彪标,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏掷豺。R本人自食惡果不足惜捞烟,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望当船。 院中可真熱鬧题画,春花似錦、人聲如沸德频。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至竞思,卻和暖如春表谊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盖喷。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工爆办, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人课梳。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓距辆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親暮刃。 傳聞我的和親對象是個殘疾皇子跨算,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 0. 前言 JVM筆記系列,以JDK1.7為基準椭懊,主要以《深入理解Java虛擬機》(第二版)和《Java虛擬機規(guī)范...
    郭尋撫閱讀 910評論 0 3
  • GC區(qū)域Eden Survivor(from,to), Old Gen和Perm Gen VM區(qū)域總體分兩類诸蚕,he...
    Fitz_Lee閱讀 416評論 0 0
  • 注意 : 本系列文章為學習系列,部分內(nèi)容會取自相關書籍或者網(wǎng)絡資源,在文章中間和末尾處會有標注 垃圾回收的意義 它...
    lyk2112閱讀 331評論 0 0
  • 我今天用那道很多機構(gòu)都用的那五條數(shù)學題給笑媽聊人性。 不知道各位看過沒氧猬,就是: 1+1=2 2+1=3 2+2=5...
    羽翼成閱讀 506評論 0 50
  • 我是日記星球357號星寶寶清遠仙人掌挫望,我正在參加日記星球第13期21天蛻變之旅,這是我的第2篇原創(chuàng)日記~ 關注日記...
    清遠仙人掌閱讀 1,072評論 0 5