美文网首页js
JavaScript 内存管理

JavaScript 内存管理

作者: 抽疯的稻草绳 | 来源:发表于2020-09-22 21:58 被阅读0次

    作为一个 JavaScript 的开发者,大多数情况下你可能不会担心内存管理问题,因为 JavaScript 引擎会帮你处理这些。但是在开发过程中,你或多或少的会遇到一些相关的问题,比如内存泄漏等,只有了解了内存分配的工作机制,你才会知道如何去解决这些问题。

    在这篇文章中,我将会向你介绍 内存分配垃圾收集 的机制,以及如何避免一些 常见的内存泄漏 的问题。

    内存生命周期

    在 JavaScript 中,当我们创建变量、函数或者其他东西的时候,JS 引擎会自动的为它分配内存,当它不再被使用的时候,JS 引擎又会自动的去释放掉这块内存。

    分配内存,实际上是在内存中保留一块空间的过程,而 释放内存 则是释放这块区域的空间,以便后续使用。

    每次我们给变量赋值或者创建一个函数的时候,它所对应的那块内存总会经历如下的阶段:

    图片
    • 内存分配

    JavaScript 会帮我们处理,它会为我们创建的内容分配内存。

    • 内存使用

    使用内存的过程体现在代码中,我们对于变量或对象等的读写其实就是对内存的读写。

    • 内存释放

    这一步也是由 JavaScript 引擎处理的。一旦这个内存被释放掉了,它就可以用于新的目的。

    堆内存和栈内存

    现在我们知道了,在 JavaScript 中定义的任何东西,JS 引擎都会为他分配内存,并且在不再使用的时候释放掉。

    接下来我们要考虑的问题就是:我们创建的变量、函数等,会被存放在哪里呢?

    JavaScript 引擎有两个地方可以存储数据:堆内存栈内存

    堆(Heap)栈(Stack) 是两种不同的数据结构,他们的使用场景也各不相同。

    栈:静态内存分配

    图片

    js stack memory

    栈是 JavaScript 用来存放 静态数据 的一种数据结构。静态数据指的是 JS 引擎在编译时期就能确定其大小的数据。在 JS 中,它包括 原始的值(strings, numbers, booleans, undefined, symbol, and null)和 指向对象和函数的 引用

    由于引擎知道了数据的大小不会再改变了,那么在分配内存的时候,就会给它分配一个 固定大小 的空间。

    在程序执行前分配内存的过程,就叫做 静态内存分配

    因为引擎为这些值分配的是固定大小的内存,所以这些值的大小肯定是有个上限的,而这个上限取决于具体的浏览器。

    堆:动态内存分配

    堆内存是 JavaScript 用来存在对象和函数的区域。与栈内存不同的是,引擎并不会为这些对象分配一个固定大小的内存,相反,它将根据具体的需要来分配对应的内存空间,这种内存分配的方式就是 动态内存分配

    我们来对比一下栈和堆内存的区别:

    |
    | 栈(Stack) | 堆(Heap) |
    | --- | --- | --- |
    | 值类型 | 原始值和引用 | 对象和函数 |
    | 时期 | 编译期间确定大小 | 运行期间确定大小 |
    | 大小 | 固定大小 | 无具体限制 |

    例子

    // 为对象分配堆内存
    const person = {
      name: 'John',
      age: 24,
    };
    
    // 数组也是对象,所以分配的也是堆内存
    const hobbies = ['hiking', 'reading'];
    
    let name = 'John'; // 为字符串分配栈内存
    const age = 24; // 为数字分配栈内存
    
    name = 'John Doe'; // 为字符串分配新的栈内存
    const firstName = name.slice(0,4); // 为字符串分配新的栈内存
    

    这里要注意的是,原始值都是不可变的,所以修改的时候实际上是创建了一个新的值。

    JavaScript 中的引用

    所有的变量一开始都是指向栈的。如果它不是原始值,那么栈中保留着指向堆内存中对象的引用。

    堆内存里的数据并不是按照某个特定的顺序排列的,所以我们需要在栈中保留一个指向堆内存数据的引用。您可以将引用当作是地址,而堆内存中的对象则是这些地址所对应的房屋。

    图片

    上图清晰的展示了不同类型的值是如何存放的。要注意的是,personnewPerson 都是指向同一个对象的。

    垃圾收集

    这里已经知道了,JavaScript 会为所有类型的数据分配内存,但是如果你还记得一开始介绍的内存生命周期,你就知道我们还缺少最后一步:内存释放。

    与内存分配一样,这一步也是由 JS 引擎为我们完成的,更具体的说,是 垃圾收集器 为我们完成的。

    当 JS 引擎识别到给定变量或函数不再需要的时候,它就会释放其所占用的内存。

    这一步骤的主要问题在于,我们无法精确的判定某一块内存是仍然需要的,这 只能是一个近似的过程,无法通过算法来解决。这里介绍两种最常见的算法:引用计数法 和 标记清除法(注意,它们也都是最大程度的近似判定)。

    引用计数法

    这是最简单的实现,它收集 没有引用指向它们的 对象作为垃圾。来看一下下面的演示:

    image.gif

    <figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">reference-couting</figcaption>

    这里要注意,在最后一帧中,只有 hobbies 保留在堆内存中,因为它是唯一一个有引用指向他的对象。

    循环引用

    引用计数法的问题在于,它没有考虑到循环引用的场景。当一个或多个对象之间相互引用,并且不能通过代码访问它们时,就会发生这种情况。看下面的例子:

    let son = {
      name: 'John',
    };
    
    let dad = {
      name: 'Johnson',
    }
    
    son.dad = dad;
    dad.son = son;
    
    son = null;
    dad = null;
    
    图片

    <figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">reference cycle</figcaption>

    由于 son 和 dad 这两个对象都引用了对方,所以这个算法不会释放它们占用的内存,我们也无法通过代码来访问到这两个对象。将它们都设置为 null 也无济于事,因为都有引用指向它们,所以标记清除法照样会认为它们是有用的,不可回收。

    标记清除法

    标记清除法很好的避免了循环引用的问题。它假定了一个叫做根(root)的对象,然后从它出发去访问给定的对象。根对象在浏览器中是 window 对象,在 NodeJS 中是 global 对象。

    图片

    <figcaption style="margin: 5px 0px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">garbage-collectoion-algorithm</figcaption>

    该算法将 不可访问的对象 标记为垃圾,然后 清除(收集)它们。根对象将永远不会被收集。这样,循环引用就不再是个问题了。在之前的例子中,dad 和 son 这两个对象最后都无法通过根对象访问到,所以它们都会被标记为垃圾然后被清理掉。

    从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记-清除算法性能和实现的改进,并不是对算法本身。

    权衡

    自动的垃圾收集机制让我们可以专注于构建应用程序本身,而不用因为内存管理而浪费时间。然而,我们需要注意一些权衡。

    内存使用

    由于算法无法确切的知道何时不再需要某块内存,所以 Javascript 应用可能会比平时需要更多的内存

    即使某些对象已经被标记为垃圾了,但具体的垃圾收集时机还是由垃圾收集器来决定的。

    如果你想你的应用程序尽可能地提高内存效率,那你最好使用一些底层(lower-level)语言。但请记住,任何语言的内存管理都有自己的一套权衡。

    性能

    为我们收集垃圾的算法通常是定期运行的。然而问题是,作为开发者,我们并不知道它什么时候发生。收集大量的垃圾或频繁地收集垃圾可能会影响性能,因为这样做需要一定的计算能力。当然,我们的用户或开发人员通常不会注意到这种影响。

    内存泄漏

    好了,有了上面的知识储备,下面我们来看看几种常见的内存泄漏问题。当你理解背后的原理时,你就会发现这些问题都可以轻松的避免。

    全局对象

    将数据存储在全局变量上可能是最常见的内存泄漏问题了。举个例子,在浏览器中声明一个变量,如果你不用 const 或者 let,而是用 var 或者干脆省略关键字,那么这个变量将会变成 window 对象的一个属性。用 function 定义的函数也同理。

    major = 'JS';
    var user = 'Jerry';
    function getName() {
      return 'jerry';
    }
    
    window.major // => 'JS'
    window.user // => 'Jerry'
    window.getName() // => 'jerry'
    

    这只适用于在全局作用域中定义的变量和函数,关于 JS 作用域的内容你可以参考这篇文章。

    你可以在 严格模式 下运行你的代码,这样可以避免上述问题。

    当然有时候你可能是故意的使用全局变量来存储一些信息,但是请确保在不再需要这些对象的时候主动的设置为 null,这样可以保证垃圾收集器可以及时的回收掉它的内存:

    window.user = null;
    

    被遗忘的定时器与回调函数

    忘记处理了某些计时器和回调函数会增加应用程序的内存。特别是在单页应用程序(SPA)中,在动态添加事件监听和回调时务必要小心。

    定时器

    const object = {};
    const intervalId = setInterval(function() {
      doSomething(object);
    }, 2000);
    

    这段代码每两秒执行一次,定时器内部引用了外部的 object 对象。只要定时器在运行,这个 object 对象就不会被回收。所以要确保在合适的时机清除掉这个定时器:

    clearInterval(intervalId);
    

    这点在 SPA 中特别重要。因为有时候你可能已经导航到另一个页面去了,但是原先页面的定时器还在后台运行着,它导致了引用了外部对象无法被回收。

    回调函数

    假设你有一个按钮,它绑定了一个 onclick 事件。

    一些老的浏览器的垃圾回收器是无法收集监听器的,不过现在基本都可以了,不过还是建议你在不需要的时候,手动的移除事件监听,释放内存。

    const element = document.getElementById('button');
    const onClick = () => alert('hi');
    
    element.addEventListener('click', onClick);
    
    element.removeEventListener('click', onClick);
    element.parentNode.removeChild(element);
    

    DOM 引用

    这种内存泄漏与上一个相似,它们都发生在存储 DOM 元素的时候。

    const elements = [];
    const element = document.getElementById('button');
    elements.push(element);
    
    function removeAllElements() {
      elements.forEach((item) => {
        document.body.removeChild(document.getElementById(item.id))
      });
    }
    

    当你删除某一个元素的时候,你可能希望从 elements 数组中也删除对应的元素。否则,这些 DOM 元素还是不能被垃圾收集器收集。

    const elements = [];
    const element = document.getElementById('button');
    elements.push(element);
    
    function removeAllElements() {
      elements.forEach((item, index) => {
        document.body.removeChild(document.getElementById(item.id));
        // 从数组中删除
        elements.splice(index, 1);
      });
    }
    

    相关文章

      网友评论

        本文标题:JavaScript 内存管理

        本文链接:https://www.haomeiwen.com/subject/slciyktx.html