zoukankan      html  css  js  c++  java
  • JavaScript js调用堆栈(三)

    本文主要深入介绍JavaScript内存机制

    内存模型

    JS内存空间分为栈(stack)堆(heap)池(一般也会归类为栈中),其中存放变量,存放复杂对象,存放常量。

    注:闭包中的变量并不保存在栈内存中,而是保存在堆内存中,这就是函数之后为什么闭包还能引用函数内的变量的原因。

    function A() {
      let a = 1
      function B() {
          console.log(a)
      }
      return B
    }

    闭包的简单定义是:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

    函数 A Push调用栈后,函数 A 中的变量这时候是存储在堆上的,所以函数B依旧能引用到函数A中的变量。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

    下面重点讲解内存回收内存泄漏

    内存回收

    JavaScript有自动垃圾收集机制,就是找出那些不在继续使用的值。然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。最常用的是标记清除法,例如 a=null 其实只是一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。并且在适当的时候解除引用,是为页面获得更好性能的一个重要方式。

    局部变量全局变量的回收

    • 局部变量:在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收;
    • 全局变量:全局变量什么时候需要自动释放内存内存空间很难判断,因此在开发中应尽量避免使用全局变量,以确保性能问题。

    Google的V8引擎为例,在V8引擎中所有的JAVASCRIPT对象都是通过来进行内存分配的

    • 初始分配:当我们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量;
    • 继续申请:如果已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。

    V8引擎对堆内存中的JAVASCRIPT对象进行分代管理

    • 新生代:新生代即存活周期较短的JAVASCRIPT对象,如临时变量、字符串等;
    • 老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

    一段代码分析一下垃圾回收:

    function fun1() {
        var obj = {name: 'lhh', age: 21};
    }
     
    function fun2() {
        var obj = {name: 'coder', age: 2}
        return obj;
    }
     
    var f1 = fun1();
    var f2 = fun2();

    在上述代码中,当执行 var f1 = fun1(); 的时候,执行环境会创建一个 {name:'lhh',age:21} 这个对象,当执行 var f2 = fun2(); 的时候,执行环境会创建一个 {name:'coder',age:2} 这个对象,然后在下一次垃圾回收执行的时候,会释放 {name:'lhh',age:21} 这个对象的内存,但并不会释放 {name:'coder',age:100} 这个对象的内存。这就是因为在 fun2() 函数中将 {name:'coder',age:2} 这个对象返回,并且将其引用赋值给了 f2 变量,又由于 f2 这个对象属于全局变量,所以在页面存在的情况下, f2 所指向的对象 {name:'coder',age:2} 是不会被回收的。(由于JavaScript语言的特殊性,例如闭包...,导致如何判断一个对象是否会被回收比较困难,以上仅供参考)

    垃圾回收算法

    对垃圾回收算法来说,核心思想就是如何判断内存已经不再使用,常用垃圾回收算法有下面两种。

    • 引用计数(现代浏览器不再使用)

    • 标记清除(常用)

    引用计数

    引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明该对象已经不再需要了。

    // 创建一个对象person,他有两个指向属性age和name的引用
    var person = {
        age: 12,
        name: 'aaaa'
    };
    
    person.name = null; // 虽然设置为null,但因为person对象还有指向name的引用,因此name不会回收
    
    var p = person; 
    person = 1;         //原来的person对象被赋值为1,但因为有新引用p指向原person对象,因此它不会被回收
    
    p = null;           //原person对象已经没有引用,很快会被回收

    由上面可以看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。

    function cycle() {
        var o1 = {};
        var o2 = {};
        o1.a = o2;
        o2.a = o1; 
    
        return "Cycle reference!"
    }
    
    cycle();

    cycle函数执行完成之后,对象o1o2实际上已经不再需要了,但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收。所以现代浏览器不再使用这个算法。但IE依旧使用。

    var div = document.createElement("div");
    div.onclick = function() {
        console.log("click");
    };

    上面写法创建一个DOM元素并绑定一个点击事件,这里的变量div有事件处理函数的引用,同时事件处理函数也有div的引用(变量div可在函数内被访问),所以循环引用就出现了。

    标记清除(常用)

    标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的,给予保留。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

    从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。但反之未必成立。

    算法由以下几步组成:

    1. 垃圾回收器创建了一个“roots”列表。roots 通常是代码中全局变量的引用。JavaScript 中,“window” 对象是一个全局变量,被当作 root 。window 对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);
    2. 所有的 roots 被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从 root 开始的所有对象如果是可达的,它就不被当作垃圾;
    3. 所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。

    现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。

    根据这个概念,上面的例子可以正确被垃圾回收处理了。

    当div与其事件处理函数不能再从全局对象出发触及的时候,垃圾回收器就会标记并回收这两个对象。

    内存泄漏

    例子:

    对于主流浏览器来说,只需要切断需要回收的对象与根部的联系。最常见的内存泄露一般都与DOM元素绑定有关:

    email.message = document.createElement(“div”);
    displayList.appendChild(email.message);
    
    // 稍后从displayList中清除DOM元素
    displayList.removeAllChildren();

    上面代码中,div元素已经从DOM树中清除,但是该div元素还绑定在email对象中,所以如果email对象存在,那么该div元素就会一直保存在内存中。

    概念:

    对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

    内存泄漏识别方法:

    1. 浏览器方法
      • 打开开发者工具,选择 Memory
      • 在右侧的Select profiling type字段里面勾选 timeline
      • 点击左上角的录制按钮
      • 在页面上进行各种操作,模拟用户的使用情况。
      • 一段时间后,点击左上角的 stop 按钮,面板上就会显示这段时间的内存占用情况。
    2. 命令行方法

          使用Node提供的process.memoryUsage方法

    console.log(process.memoryUsage());
    
    // 输出
    { 
      rss: 27709440,        // resident set size,所有内存占用,包括指令区和堆栈
      heapTotal: 5685248,   // "堆"占用的内存,包括用到的和没用到的
      heapUsed: 3449392,    // 用到的堆的部分
      external: 8772         // V8 引擎内部的 C++ 对象占用的内存
    }

    判断内存泄漏,以heapUsed字段为准

    WeakMap

    ES6 新出的两种数据结构:WeakSet 和 WeakMap,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。

    const wm = new WeakMap();
    const element = document.getElementById('example');
    
    wm.set(element, 'some information');
    wm.get(element) // "some information"

    先新建一个 Weakmap 实例,然后将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。

     四种常见的JS内存泄漏

    1.意外的全局变量

    未定义的变量会在全局对象创建一个新变量,如下:

    function foo(arg) {
        bar = "this is a hidden global variable";
    }

    真相是:

    function foo(arg) {
        window.bar = "this is an explicit global variable";
    }

    函数 foo 内部忘记使用 var ,实际上JS会把bar挂载到全局对象上,意外创建一个全局变量。

    另一个意外的全局变量可能由 this 创建:

    function foo() {
        this.variable = "potential accidental global";
    }
    
    // Foo 调用自己,this 指向了全局对象(window)
    // 而不是 undefined
    foo();

    解决方法:在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓存内容无法被回收。

    2.被遗忘的计时器或回调函数

    在 JavaScript 中使用 setInterval 非常常见:

    var someResource = getData();
    setInterval(function() {
        var node = document.getElementById('Node');
        if(node) {
            // 处理 node 和 someResource
            node.innerHTML = JSON.stringify(someResource));
        }
    }, 1000);

    上面的例子表明,在节点node或者数据不再需要时,定时器依旧指向这些数据。所以哪怕当node节点被移除后,interval 仍旧存活并且垃圾回收器没办法回收,它的依赖也没办法被回收,除非终止定时器。

    var element = document.getElementById('button');
    function onClick(event) {
        element.innerHTML = 'text';
    }
    
    element.addEventListener('click', onClick);

    对于上面观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。因为老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。

    3.脱离DOM的引用

    有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。

    var elements = {
        button: document.getElementById('button'),
        image: document.getElementById('image'),
        text: document.getElementById('text')
    };
    function doStuff() {
        image.src = 'http://some.url/image';
        button.click();
        console.log(text.innerHTML);
        // 更多逻辑
    }
    function removeButton() {
        // 按钮是 body 的后代元素
        document.body.removeChild(document.getElementById('button'));
        // 此时,仍旧存在一个全局的 #button 的引用
        // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
    }

    此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td> 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 以外的其它节点。实际情况并非如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td> 的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。

    4.闭包

    闭包的关键是匿名函数可以访问父级作用域的变量。

    var theThing = null;
    var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
        if (originalThing)
          console.log("hi");
      };
    
      theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
          console.log(someMessage);
        }
      };
    };
    
    setInterval(replaceThing, 1000);

    每次调用 replaceThing theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。最重要的事情是,闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。someMethod 可以通过 theThing 使用,someMethod 与 unused 分享闭包作用域,尽管 unused从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄漏。

    解决方法:在 replaceThing 的最后添加 originalThing = null

    推荐阅读:

    https://juejin.im/post/5b10ba336fb9a01e66164346#comment

    https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them//

  • 相关阅读:
    webpack常见的配置项
    详解javascript立即执行函数表达式(IIFE)
    javascript闭包—围观大神如何解释闭包
    hubilder打包+C#服务端个推服务实现
    vue学习笔记1-基本知识
    javascript中的字典
    javascript中获取元素尺寸
    php常见知识
    javascript中使用循环链表实现约瑟夫环问题
    ASP.NET Core 中的文件上传
  • 原文地址:https://www.cnblogs.com/lhh520/p/10180589.html
Copyright © 2011-2022 走看看