一文读懂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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章