一文讀懂V8垃圾回收機制——新生代Scavenge、老生代Mark-Sweep和Mark-Compact

1 V8內存管理

1.1 V8n內存限制

  • 64位系統可用1.4G內存
  • 32位系統可用0.7G內存

1.2 V8內存管理

  • JS對象都是通過V8進行分配管理內存的
  • process.memoryUsage()返回一個對象,包含了Node進程的內存佔用信息

    console.log(process.memoryUsage());
    //結果如下:
    {
        rss: 19550208,  // 所有內存佔用,包括指令區和堆棧
        heapTotal: 5533696, // “堆”佔用的內存,包括用到的和沒用到的
        heapUsed: 2645088, // 用到的堆的部分。判斷內存泄漏,以heapUsed字段爲準
        external: 778349 // V8引擎內部的C++對象佔用的內存
    }
  • 所有內存佔用結構圖:

    ------------------------------------------
    |                                        |
    |       Resident Set 所有內存佔用          |
    |                                        |
    |      ----------------------------      |
    |      |  代碼區域 Code Segment     |      |
    |      ----------------------------      |
    |                                        |
    |      ----------------------------      |
    |      |  (Stack):本地變量、指針  |      |
    |      ----------------------------      |   
    |                                        |
    |      ----------------------------      |
    |      |  HeapTotal():對象,閉包  |      |
    |      |                          |      |
    |      |   ---------------------  |      |   
    |      |   |heapUsed 使用到的堆。|  |      | 
    |      |   |     判斷內存泄漏,  |  |      |
    |      |   |   以heapUsed字段爲準|  |      |
    |      |   ---------------------  |      |   
    |      |                          |      |
    |      ----------------------------      |
    ------------------------------------------

  • 上圖舉例:
    • var a = {name:‘yuhua’};這句代碼會做如下幾步:

      • 將這句代碼放入“代碼區域 Code Segment”
      • 將變量a放入“棧(Stack):本地變量、指針”
      • 將{name:‘yuhua’}放入“HeapTotal(堆):對象,閉包”
    • 注意:基本數據類型都在棧中,引用類型都在堆中

1.3 爲何限制內存大小

  • 因爲V8垃圾收集工作原理導致的,1.4G內存完全一次垃圾收集需要1s以上
  • 這個垃圾回收這段時間(暫停時間)成爲Stop The World,在這期間,應用的性能和響應能力都會下降

1.4 如何打開內存限制

  • 一旦初始化成功,生效後不能修改
  • -max-new-space-size 最大 new space 大小,執行scavenge回收,默認16M,單位KM
  • -max-old-space-size,最大 old space 大小,執行MarkSweep回收,默認1G,單位MB

2.V8的垃圾回收機制

  • V8是基於分代的垃圾回收
  • 不同代垃圾回收機制也不一樣
  • 按存貨的時間分爲新生代和老生代

2.1 分代

  • 年齡小的是新生代,由From區域和To區域兩個區域組成
    • 在64位系統裏,新生代內存是32M,From區域和To區域各佔16M
    • 在32位系統裏,新生代內存是16M,From區域和To區域各佔8M
  • 年齡大的是老生代,默認情況下:
    • 64位系統下老生代內存是1400M
    • 32位系統下老生代內存是700M

2.2 新生代的垃圾回收(新生代GCGC)

2.2.1 過程
  • 新生代區域一分爲二,每個16M,一個使用,一個空閒
  • 開始垃圾回收的時候,會檢查FROM區域中的存活對象,如果還活着,拷貝到TO空間,所有存活對象拷貝完後,清空(釋放)FROM區域
  • 然後FROM和To區域互換
2.2.2 特點
  • 新生代掃描的時候是一種廣度優先的掃描策略

    • 什麼叫做廣度優先的掃描策略?這裏涉及到掃描指針跟分配指針
      • 舉例:
        • 假設全局變量下的變量A引用了變量B和變量C,變量B引用了變量D,變量E沒被引用
            ROOTA          E
            ↓→ → →
            ↓    ↓
            B    CD
        
        • 那麼在to區域,存在一個掃描指針和分配指針,起初都指向最開始
        • 然後A發現被全局引用,那麼A拷貝到to區域,這時候掃描指針指向A,分配指針指向後面一位
            A
            ↑   ↑
            SM  FP
        
        • 然後A又引用了B,這時候把B拷貝到to區域,放在A後面,掃描指針還是指向A,分配指針往後移動一位
            A   B
            ↑       ↑
            SM     FP
        
        • 然後再看A又引用了C,這時候把C拷貝到to區域,放在B後面,掃描指針還是指向A,分配指針往後移動一位
            A   B   C  
            ↑           ↑
            SM          FP
        
        • 然後發現A沒有引用其他了,那將掃描指針往後移動一位指向B
            A   B    C
                ↑        ↑
                SM       FP
        
        • 然後發現B沒有引用其他了,那再將掃描指針往後移動一位指向C
            A   B    C
                     ↑     ↑
                     SM    FP
        
        • 然後C也沒有引用其他了,還剩下D,D不存在引用,所以不需要移動到to區域,這時候清空FROM區域,然後將FROM和To交換。
        • 這就完成了一次垃圾回收。
  • 新生代的空間小,存活對象少

  • 當一個對象經理多次的垃圾回收依然存活的時候,生存週期比較差的對象會被移動到老聲帶,這個移動過程被稱爲晉升或升級

    • 經歷過5次以上的回收還存在
    • TO的空間使用佔比超過25%,或者超大對象
  • 瀏覽器的memory中可以通過拍快照看變量是否被垃圾回收

  • 置爲undefined 或 null 都能將引用計數減去1

2.2.3 舉例
  • 例一:
    function Person(){
        this.name = name;
    }
    let p1 = new Person('yuhua1');// 引用計數爲1
    let p2 = new Person('yuhua2');// 引用計數爲1
    // p1 p2 不會被自動銷燬,因爲全局(雖然是let),如果是局部,會自動銷燬
  • 例二:
    function Person(){
        this.name = name;
    }
    let p1 = new Person('yuhua1');// 引用計數爲1
    let p2 = new Person('yuhua2');// 引用計數爲1
    setTimeout(function(){
        p1 = null;// 3秒後引用計數減1,變成0,就銷燬,
    }, 3000)
    setTimeout(function(){
        p1 = null;// 10秒後引用計數減1,變成0,就銷燬,
    }, 10000)
  • 例三:
    function Person(name){
        this.name = name;
    }
    let set = new Set();
    let p1 = new Person('yuhua');//引用計數爲1
    set.add(p1);//引用計數爲2
    p1 = null;//引用計數減1,爲1,並不會銷燬,雖然p1確實變爲null了
    //這時候怎麼銷燬p1,不能再將p1置爲null,這是沒用的。由於p1是被set引用,所以應該將set銷燬,這樣才能銷燬p1
    set = null;//這時候p1引用計數減1,爲0,銷燬。同時set引用計數也爲0,也銷燬了

    
  • 例四:
    function Person(name){
        this.name = name;
    }
    let PersonFactory = function(name){
        let p = new Person(name);
        return function(){
            console.log(p);
        }
    }
    let p1 = PersonFactory('zfpx');// 這裏的p引用計數爲1
    p1();//這裏的p引用計數爲2

2.3 老生代的垃圾回收策略

2.3.1 基礎
  • 老生代垃圾回收策略分爲兩種
    • mark-sweep 標記清除
      • 標記活着的對象,雖然清楚在標記階段沒有標記的對象,只清理死亡對象
      • 會出現的問題:清除後內存不連續,碎片內存無法分配
    • mark-compact 標記整理
      • 標記死亡後會對對象進行整理,活着的左移,移動完成後清理掉邊界外的內存(死亡的對象)
  • 老生代空間大,大部分都是活着的對象,GC耗時比較長
  • 在GC期間無法想聽,STOP-THE-WORLD
  • V8有一個優化方案,增量處理,把一個大暫停換成多個小暫停 INCREMENT-GC
    • 也就是把大暫停分成多個小暫停,每暫停一小段時間,應用程序運行一會,這樣垃圾回收和應用程序交替進行,停頓時間可以減少到1/6左右
2.3.2 過程

假設有10個大小的內存,內存佔用了6個,

  • Mark-Sweep模式垃圾回收:
    • 那麼會給每個對象做上標記:如下圖
    
        A   b   C   d   E   f  空  空 空 空
        //對上面每個對象做上標記,大寫表示活着,小寫表示死了
        //這時候,會存在一個問題,就是內存碎片無法使用,因爲小寫的內存沒有跟後面空空空空的內存放在一起,不能使用
    
    
    • 這時候小寫(死)的都會被幹掉,只保留大寫(活)的,導致的問題就是內存碎片無法使用
  • Mark-Compact模式垃圾回收:
    • 將活的左移
        A C E b d f 空 空 空 空
    
    • 然後回收死了的區域
        A C E 空 空 空 空 空 空 空
    

2.4 三種垃圾回收算法對比

回收算法 Mark-Sweep Mark-Compact Scavenge
速度 中等 最慢 最快
空間開銷 雙倍空間(無碎片)
是否移動對象
  • V8老生代主要用Mark-Sweep,因爲Mark-Compact需要移動對象,執行速度不快。空間不夠時,纔會用Mark-Compact
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章