zoukankan      html  css  js  c++  java
  • JS 的内存管理-GC

    1. JS与C#, Java,Python等很多高级语言一样,对堆中内存有虚拟机(V8)自动回收,不需要程序员手动释放,让coder们更多的focus on业务代码。

    2. 对于栈中的基础数据类型,当前函数执行完,只需要移动ESP(记录当前执行状态)指针,则当前执行栈中的上下文全部销毁,所有变量会自动无效,这样的效率非常高。

      问题:栈中对象回收那么高效,为何还有把对象存储到堆中?

      因为 栈是计算机底层提供的一种数据结构,一般比较小的寄存器来存储,加快cpu读写,相对较小,所以只适合哪些体积较小,生命周期很短的对象,所以函数无限递归或者嵌套层次过深,就会出现栈溢出。

    3. GC的代际假说

    • 大部分对象都是“朝生夕死”的,也就是说大部分对象在内存中存活的时间很短,比如函数内部声明的变量,或者块级作用域中的变量,当函数或者代码块执行结束时,作用域中定义的变量就会被销毁。因此这一类对象一 经分配内存,很快就变得不可访问;
    • 不死的对象,会活得更久,比如全局的 window、DOM、Web API 等对象。

    4.  目前 V8 采用了两个垃圾回收器,主垃圾回收器 -Major GC 和副垃圾回收器 -Minor GC (Scavenger)。V8会把堆分为新生代和老生代两个区域,新生代(1-8M)中存放的是生存时间短的对象,老生代中存放生存时间久的对象,以便更高效地实施垃圾回收。副垃圾回收器 -Minor GC (Scavenger),主要负责新生代的垃圾回收。主垃圾回收器 -Major GC,主要负责老生代的垃圾回收。

    5. 副垃圾回收器

    内存占用比较少,回收频率高,它使用Scavenger算法,这个算法把新生代分成2个区,对象区域和空闲区域。新申请的内存都在对象区域,对象区域内存阈值达到之后,执行GL,没有被回收的对象被有序复制放入空闲区域(连续的),然后对换角色,2次GC之后,还留下来的对象晋升到老生代中。

    One important thing - 主要的回收过程是怎么样的?

    • 第一步:GC回收器会根据Root objects (全局的 window 对象,存放栈上变量,文档 DOM 树,可以通过遍历文档到达的所有原生 DOM 节点组成)作为起点去查找可到达的对象,可达则是活动对象,不可到达,说明是没有被引用的对象,则是非活动对象,所有非活动对象则是可回收对象。
    • 第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
    • 第三步,做内存整理(主垃圾回收才有的步骤)。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。但这步其实是可选的,因为副垃圾回收器不会产生内存碎片。

    6. 主垃圾回收器

    晋升来的对象,还有大对象会直接在老生代中申请分配。

    由于老生代的对象比较大,若要在老生代中使用 Scavenger 算法进行垃圾回收,复制这些大的对象就会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。所以,主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的。不过对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片,而碎片过多会导致大对象无法分配到足够的连续内存,于是又引入了另外一种算法——标记 - 整理(Mark-Compact),实际上就是多了一步内存碎片整理。

    7. 需要注意的点

    传统的垃圾回收器,是全停顿操作,且占用JS主线程的时间片,意思是说JS脚本会停下来等待垃圾回收结束才能继续执行,所以界面往往会出现卡顿,不流畅的情况。So,V8 在不断的迭代升级中,也逐步优化这个GC过程。

    • 第一个方案是并行回收,在执行一个完整的垃圾回收过程中,垃圾回收器会使用多个辅助线程来并行执行垃圾回收。注意这里多个辅助线程来执行与主线程之间存在竞争,所以移动对象后,需要同步控制,V8的副垃圾回收器会使用这个方式,同步操作还是全停顿模式。
    • 第二个方案是增量式垃圾回收,垃圾回收器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。采用增量垃圾回收时,垃圾回收器没有必要一次执行完整的垃圾回收过程,每次执行的只是 整个垃圾回收过程中的一小部分工作。为了解决这个问题,V8 采用了三色标记法,除了黑色和白色,还额外引入了灰色:黑色表示这个节点被 GC Root 引用到了,而且该节点的子节点都已经标记完成了 ;灰色表示这个节点被 GC Root 引用到,但子节点还没被垃圾回收器标记处理,也表明目前正在处理这个节点;白色表示这个节点没有被访问到,如果在本轮遍历结束时还是白色,那么这块数据就会被收回。引入灰色标记之后,垃圾回收器就可以依据当前内存中有没有灰色节点,来判断整个标记是否完成,如果没有灰色节点了,就可以进行清理工作了。如果还有灰色标记,当下次恢复垃圾回收器时,便从灰色的节点开始继续执行。因此采用三色标记,可以很好地支持增量式垃圾回收。
    • 第三个方案是并发回收,回收线程在执行 JavaScript 的过程,辅助线程能够在后台完成的执行垃圾回收的操作。这种方案最好,但实现起来最难,因为它完全与JS主线程独立开来,互相不影响,需要克服实际运行环境中的很多问题。

    主垃圾回收器就综合采用了所有的方案,副垃圾回收器也采用了部分方案。

    8. JavaScript 中的几种常见的内存问题

    a. 内存泄漏 (Memory leak),它会导致页面的性能越来越差;

    在 JavaScript 中,造成内存泄漏 (Memory leak) 的主要原因是不再需要 (没有作用) 的内存数据依然被其他对象引用着。

    比如非严格模式下,改在window对象下的全局对象。

    我们还要时刻警惕闭包这种情况,因为闭包会引用父级函数中定义的变量,如果引用了不被需要的变量,那么也会造成内存泄漏

    function foo(){  
        var temp_object = new Object()
        temp_object.x = 1
        temp_object.y = 2
        temp_object.array = new Array(200000)
        /**
        *   使用temp_object
        */
        return function(){
            console.log(temp_object.x);
        }
    }

    这里我们的闭包引用的temp_object这整个大对象,实际使用的只有它的x,但整个大对象还是会被缓存起来,就造成了内存泄漏。so,用临时变量来存储一下x,就可以避免,like below code。

    function foo(){  
        var temp_object = new Object()
        temp_object.x = 1
        temp_object.y = 2
        temp_object.array = new Array(200000)
        /**
        *   使用temp_object
        */
       let closure = temp_object.x
        return function(){
            console.log(closure);
        }
    }

    JavaScript 引用了 DOM 节点而造成的内存泄漏的问题,只有同时满足 DOM 树和 JavaScript 代码都不引用某个 DOM 节点,该节点才会被作为垃圾进行回收。 如果某个节点已从 DOM 树移除,但 JavaScript 仍然引用它,我们称此节点为“detached ”。“detached ”节点是 DOM 内存泄漏的常见原因。

    我们通过 JavaScript 创建了一些 DOM 元素,有了这些内存中的 DOM 元素,当有需要的时候,我们就快速地将这些 DOM 元素关联到 DOM 树上,一旦这些 DOM 元素从 DOM 上被移除后,它们并不会立即销毁,这主要是由于 JavaScript 代码中保留了这些元素的引用,导致这些 DOM 元素依然会呆在内存中。所以在保存 DOM 元素引用的时候,我们需要非常小心谨慎。

    有时候我们使用ES6的WeakMap,WeakSet就是这个原因。

    b. 内存膨胀 (Memory bloat),它会导致页面的性能会一直很差;

    内存膨胀和内存泄漏有一些差异,内存膨胀主要表现在程序员对内存管理的不科学,比如只需要 10M 内存就可以搞定的,有些程序员却花费了 500M 内存,典型的场景是算法问题导致空间复杂度很高。

    c. 频繁垃圾回收,它会导致页面出现延迟或者经常暂停。

    那就是频繁使用大的临时变量,导致了新生代空间很快被装满,从而频繁触发垃圾回收。频繁的垃圾回收操作会让你感觉到页面卡顿

    比如这段代码,循环里面频繁创建对象。

    function strToArray(str) {
      let i = 0
      const len = str.length
      let arr = new Uint16Array(str.length) // 引用对象被频繁
      for (; i < len; ++i) {
        arr[i] = str.charCodeAt(i)
      }
      return arr;
    }
    
    
    function foo() {
      let i = 0
      let str = 'test V8 GC'
      while (i++ < 1e5) {
        strToArray(str);
      }
    }
    
    
    foo()
  • 相关阅读:
    数据类型
    注释
    编译型语言和解释型语言
    POJ1026 Cipher(置换的幂运算)
    最短路(代码来源于kuangbin和百度)
    POJ1753 Flip Game(bfs、枚举)
    POJ1860 Currency Exchange(bellman-ford)
    【转】博弈—SG函数
    【转】欧几里得与扩展欧几里得
    HDU 5833 Zhu and 772002(高斯消元)
  • 原文地址:https://www.cnblogs.com/roy1/p/13728386.html
Copyright © 2011-2022 走看看