JVM學習筆記——自動內存管理

簡述

上篇文章中簡單介紹了JVM內部結構,線程隔離區域隨着線程而生,隨着線程而忘。線程共享區域由於是共享,因此可能多個線程都用到,不能輕易回收,與C語言不一樣,在Java虛擬機自動內存管理機制的幫助下,再也不須要爲每一個new操做去寫配對的delte/free代碼,可以幫助程序員更好的編寫代碼。那麼JVM是如何進行對象內存分配以及回收分配給對象內存呢?程序員

內存分配

幾乎全部的對象實例都分配在堆中,爲了進行高效的垃圾回收,虛擬機把堆劃分紅新生代(Young Generation)、老年代(Old Generation)。算法

新生代

新生代又分爲1個Eden區和2個survivor區(S0,S1),Eden區與Survivor區的內存大小比例默認爲8:1。多線程

  • Eden
  • Eden伊甸園,在大多數狀況下對象 優先在Eden區中分配

  • Survivor
  • Survivor倖存者,當Eden區沒有足夠內存進行分配,會觸發一次Minor GC,會將倖存的對象移動到內存區域S0區域,並清空Eden區域。當再次發生Minor GC時,將Eden和S0中倖存的對象移動到S1內存區域。

    倖存對象會反覆在S0和S1之間移動,當對象從Eden移動到Survivor或者在Survivor之間移動時,對象的GC年齡自動累加,當GC年齡超過默認閾值15時,會將該對象移動到老年代,能夠經過參數-XX:MaxTenuringThreshold 對GC年齡的閾值進行設置。

    老年代

    除了長期存活的對象會分配到老年代,還有如下狀況對象會分配到老年代:
    ①大對象(須要大量連續內存空間的Java對象)直接進入老年代,能夠經過參數
    -XX:PretenureSizeThreshold設定對象大小閾值,超過其值進入老年代
    ②若Survivor區域中全部相同GC年齡的對象大小超過Survivor空間的一半,年齡不小於該年齡的對象就直接進入老年代
    併發

    分配方法

  • 指針碰撞法
  • 假設Java堆中內存是完整的,已分配的內存和空閒內存分別在不一樣的一側,經過一個指針做爲分界點,須要分配內存時,僅僅須要把指針往空閒的一端移動與對象大小相等的距離。

  • 空閒列表法
  • 事實上,Java堆的內存並非完整的,已分配的內存和空閒內存相互交錯,JVM經過維護一個列表,記錄可用的內存塊信息,當分配操做發生時,從列表中找到一個足夠大的內存塊分配給對象實例,並更新列表上的記錄。

    對象建立是一個很是頻繁的行爲,進行堆內存分配時還須要考慮多線程併發問題,可能出現正在給對象A分配內存,指針或記錄還未更新,對象B又同時分配到原來的內存,解決這個問題有兩種方案:
    一、採用CAS保證數據更新操做的原子性;
    二、把內存分配的行爲按照線程進行劃分,在不一樣的空間中進行,每一個線程在Java堆中預先分配一個內存塊,稱爲本地線程分配緩衝(Thread Local Allocation Buffer, TLAB);
    ide

    內存回收

    如何判斷哪些對象佔用的內存須要回收?虛擬機有以下方法:
    post

  • 引用計數法
  • 給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器就減1;當計數器爲0時就表明此對象已死,須要回收。
    此方法沒法解決對象之間相互引用的問題

    this

    public class ReferenceCountingGC {
    public Object instance = null;
    
    private static final int _1MB = 1024 * 1024;
    
    /**
     * 方便GC能看清楚是否被回收
     */
    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();
    }
    複製代碼
    複製代碼public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 方便GC能看清楚是否被回收 */ 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(); } 複製代碼} 複製代碼
    結果:
    [GC 6758K->632K(124416K), 0.0016573 secs]
    [Full GC 632K->530K(124416K), 0.0148864 secs]
    從結果能夠看出這兩個對象依然被回收

  • 可達性分析算法
  • 經過一些節點開始搜索,當一個對象到 GC Roots 沒有任何引用鏈(經過路徑)時,表明該對象能夠被回收

    GC Roots的對象包括:
    ①本地變量表中引用的對象
    ②方法區中類靜態屬性引用的對象
    ③方法區中常量引用的對象
    ④Native方法引用的對象
    spa

    斷定一個對象是否可回收,至少要經歷兩次標記過程:
    ①若對象與GC Roots沒有引用鏈,則進行第一次標記
    ②若此對象重寫了finalize()方法,且還未執行過,那麼它會被放到F-Queue隊列中,並由一個虛擬機自動建立的、低優先級的Finalizer線程去執行此方法(並不是必定會執行)。finalize方法是對象逃脫死亡的最後機會,GC對隊列中的對象進行第二次標記,若該對象在finalize方法中與引用鏈上的任何一個對象創建聯繫,那麼在第二次標記時,該對象會被移出"即將回收"集合。
    自我救贖示例:線程

    public class FinalizeGC {
    
        public static FinalizeGC obj;
    
        public void isAlive() {
            System.out.println("yes, i am still alive");
        }
    
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("method finalize executed");
            obj = this;
        }
    
        public static void main(String[] args) throws Exception {
            obj = new FinalizeGC();
    
            // 第一次執行,finalize方法會自救
            obj = null;
            System.gc();
    
            Thread.sleep(500);
            if (obj != null) {
                obj.isAlive();
            } else {
                System.out.println("I'm dead");
            }
    
            // 第二次執行,finalize方法已經執行過
            obj = null;
            System.gc();
    
            Thread.sleep(500);
            if (obj != null) {
                obj.isAlive();
            } else {
                System.out.println("I'm dead");
            }
        }
    
    }
    複製代碼

    結果:
    method finalize executed
    yes, i am still alive
    I'm dead
    從結果來看,第一次GC時,finalize方法執行,在回收以前成功自我救贖
    第二次GC時,finalize方法已經被JVM調用過,因此沒法再次逃脫指針

    垃圾回收算法

    知道了如何判斷對象爲"垃圾",接下來就是如何清理這些對象

  • 標記-清除
  • 對"垃圾"對象進行標記並刪除
    算法缺點:
    效率問題,標記和清除這個兩個過程的效率都不高
    空間問題,標記清除後會產生大量不連續的內存碎片,不利於大對象分配

  • 複製算法
  • 將可用內存一分爲二,每次只用其中一塊,當一塊內存用完了,就把存活的對象複製到另外一塊去,並清空已使用過的內存空間。相對於複製算法不須要考慮內存碎片等複雜問題,只要移動指針,按順序分配內存便可。
    缺陷:總有一塊空閒區域,空間浪費

  • 標記-整理
  • 在老年代中,對象存活率較高,複製算法效率較低。基於標記-清除,讓全部存活對象都移動到一端,而後直接清理邊界之外的內存。

    垃圾收集器

    垃圾收集器組合:

  • Serial收集器
  • Serial是一個單線程,基於複製算法,串行GC的新生代收集器。在GC時必須停掉全部其餘工做線程直到它收集完成。對於單CPU環境來講,Serial因爲沒有線程交互的開銷,能夠很高效的進行垃圾收集,是Clinet模式下新生代默認的收集器

  • ParNew收集器
  • ParNew是Serial收集器的多線程版本(並行GC),除了使用多條線程進行GC之外,其他行爲與Serial同樣

  • Parallel Scavenge收集器
  • Parallel Scavenge是一個多線程,基於複製算法,並行GC的新生代收集器。其關注點在於達到一個可控的吞吐量。

    吞吐量 = 運行用戶代碼時間/(運行用戶代碼時間 + 垃圾收集時間)
    Parallel Scavenge提供兩個參數用於精確控制吞吐量:
    ① -XX:MaxGCPauseMillis 控制垃圾收集的最大停頓時間
    ② -XX:GCTimeRatio 設置吞吐量大小

  • Serial Old收集器
  • Serial Old是基於標記-整理算法的Serial收集器的老年代版本,是Client模式下老年代默認收集器

  • Parallel Old收集器
  • Parallel Old是基於標記-整理算法的Parallel收集器的老年代版本,在注重吞吐量以及CPU資源敏感的場合,能夠優先考慮Parallel Scavenge加Parallel Old收集器

  • CMS收集器
  • CMS收集器是一種以獲取最短回收停頓時間爲目標的老年代收集器(併發GC),基於標記-清除算法,整個過程分爲如下4步:
    ①初始標記:只標記與GC Roots直接關聯到的對象,仍然會Stop The World
    ②併發標記:進行GC Roots Tracing的過程,能夠和用戶線程一塊兒工做
    ③從新標記:用於修正併發標記期間因用戶程序繼續運行而致使標記產生變更的那部分對象記錄,此過程會暫停全部線程,但停頓時間,比初始標記階段稍長,遠比並發標記的時間短
    ④併發清理:清理"垃圾對象",能夠與用戶線程一塊兒工做

    CMS收集器缺點:
    ①對CPY資源很是敏感, 在併發階段,雖然不會致使用戶停頓,可是會佔用一部分線程資源(或者說CPU資源)而致使應用程序變慢,總吞吐量下降
    ②沒法處理浮動垃圾,在併發清理階段用戶線程還在運行依然會產生新的垃圾,這部分垃圾出如今標記過程以後,只能在下一次GC時回收
    ③CMS基於標記-清除算法實現,便可能收集結束會產生大量空間碎片,致使出現老年代還有很大空間剩餘,不得不提早觸發一次Full GC

  • G1收集器
  • G1垃圾收集器被視爲JDK1.7中HotSpot虛擬機的一個重要進化特徵(JDK9默認垃圾收集器),基於"標記-整理"算法實現。

    G1收集器優勢:
    ①並行與併發:充分利用多CPU來縮短Stop-The-World(停用戶線程)停頓時間
    ②分代收集:不須要其餘收集器配合,採用不一樣的方式處理新建的對象和已經存活一段時間、熬過屢次GC的舊對象來獲取更好的收集效果
    ③空間整合:由於基於"標記-整理"算法實現,避免了內存空間碎片問題,有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發一次GC
    ④可預測停頓:G1創建了可預測的停頓時間模型,能讓使用者明確指定在M毫秒時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒

    G1運行步驟:
    ①初始標記:只標記與GC Roots直接關聯到的對象,仍然會Stop The World
    ②併發標記:從GC Root開始對堆中對象進行可達性分析,找出存活對象,可與用戶線程併發執行
    ③最終標記:修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那部分標記記錄,虛擬機將對象變化記錄在線程Remembered Set Logs裏,併合併到Remembered Set中,此過程會暫停全部線程。
    ④篩選回收:對各個Region的回收價值與成本進行排序,根據用戶所指望的GC停頓時間來指定回收計劃

    注:
    並行:多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態
    併發:用戶線程與垃圾收集線程同時執行(不必定是並行,可能交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上

    感謝

    《深刻理解JAVA虛擬機》 https://www.jianshu.com/p/eaef248b5a2c

    相關文章
    相關標籤/搜索