VIP用户交流群:462197261 收藏本站北冥有鱼 互联网前沿资源第一站 助力全行业互联网+
在线客服:78895949
tonglan
  • 当前位置:
  • 深入理解 JS 垃圾回收

    建站教程 2019年11月09日 关键词:,,,

    前言

    JS之memoization,memoization 的原理是以参数作为 key,函数结果作为 value, 用对象进行缓存起来,以内存空间换 CPU 执行事件。memoization 的潜在陷阱即是严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。

    用闭包进行缓存的对象的内存空间,不会在函数执行完后被清除,在执行量大和参数多样性的情况下,会造成内存占用且得不到释放。

    于是,本篇文章就来讲讲 JS 的垃圾回收。

    JS 的垃圾回收机制的基本原理是:

    找出那些不再继续使用的变量,然后释放其占用的内存,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。

    那我们怎么知道变量是不是在继续使用呢?

    首先,局部变量的生存周期是在函数声明和执行阶段,函数执行完毕后,局部变量就没有存在的必要了。全局变量会在浏览器关闭或进程关闭才能释放。

    但还有一些场景,比如闭包,通过作用域链访问到函数外部的自由变量,使得自由变量保存在内存中,不会随着函数执行完毕而结束,以及对象的相互引用等,垃圾收集器就没这么容易判断哪个变量有用,哪个变量没用了。

    // 经典闭包
    function closure() {
    var name = "innerName";
    return function() {
    console.log(name);
    }
    }
    var inner = closure();
    inner(); // innerName;
    

    所以,对于标识无用的变量的策略可能会实现不同,但目前在浏览器中,通常有两种策略:标记清除和引用计数。

    二、标记-清除(Mark-Sweep)

    从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法, 那什么叫标记-清除呢?

    当变量进入执行环境时,就标记这个变量为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。

    垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。

    然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。

    最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

    另外,标记-清除有一个问题,就是在清除之后,内存空间是不连续的,即出现了内存碎片。如果后面需要一个比较大的连续的内存空间时,那将不能满足要求。而标记-整理(Mark-Compact)方法可以有效地解决这个问题。标记阶段没有什么不同,只是标记结束后,标记-整理方法会将活着的对象向内存的一端移动,最后清理掉边界的内存。

    三、引用计数

    另外一种不太常见的垃圾收集策略叫引用计数(Reference Counting),此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
    引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加 1,如果该变量的值变成了另外一个,则这个值得引用次数减 1,当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存。
    而引用计数的不继续被使用,是因为循环引用的问题会引发内存泄漏。

    function problem() {
    var objA = new Object();
    var objB = new Object();
    objA.someObject = objB;
    objB.anotherObject = objA;
    }

    objA 和 objB 通过各自的属性相互引用,也就是说,两个对象的引用次数都是 2。在函数执行完毕后,objA, objB 还将继续存在,因为他们的引用计数永远不会是 0。假如这个函数被多次执行,就会导致大量的内存得不到释放。

    四、NodeJs V8 中的垃圾回收机制

    在 Node 中,通过 JS 使用内存时就会发现只能使用部分内存(64 位系统下约为 1.4 GB, 32 位系统下约为 0.7 GB),这导致 Node 无法直接操作大内存对象。

    这是因为,以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要 50 毫秒以上,做一次非增量式的垃圾回收要 1 秒以上,而垃圾回收过程会引起 JS 线程暂停执行这么多时间。因此,在当时的考虑下,直接限制堆内存是一个好的选择。

    那么,在这样的内存限制下,V8 的垃圾回收机制又有什么特点?

    4.1、内存分代算法

    V8 的垃圾回收策略主要基于分代式垃圾回收机制,在 V8 中,将内存分为新生代和老生代,新生代的对象为存活时间较短的对象,老生代的对象为存活事件较长或常驻内存的对象。

    V8 堆的整体大小等于新生代所用内存空间加上老生代的内存空间,而只能在启动时指定,意味着运行时无法自动扩充,如果超过了极限值,就会引起进程出错。

    4.2 Scavenge 算法

    在分代的基础上,新生代的对象主要通过 Scavenge 算法进行垃圾回收,在 Scavenge 具体实现中,主要采用了一种复制的方式的方法—— Cheney 算法。

    Cheney 算法将堆内存一分为二,一个处于使用状态的空间叫 From 空间,一个处于闲置状态的空间称为 To 空间。分配对象时,先是在 From 空间中进行分配。

    当开始进行垃圾回收时,会检查 From 空间中的存活对象,将其复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空间和 To 空间的角色发生对换。

    当一个对象经过多次复制后依然存活,他将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用新的算法进行管理。

    还有一种情况是,如果复制一个对象到 To 空间时,To 空间占用超过了 25%,则这个对象会被直接晋升到老生代空间中。

    4.3 标记-清除和标记-整理算法

    对于老生代中的对象,主要采用标记-清除和标记-整理算法。标记-清除 和前文提到的标记一样,与 Scavenge 算法相比,标记清除不会将内存空间划为两半,标记清除在标记阶段会标记活着的对象,而在内存回收阶段,它会清除没有被标记的对象。
    而标记整理是为了解决标记清除后留下的内存碎片问题。

    4.4 增量标记(Incremental Marking)算法

    前面的三种算法,都需要将正在执行的 JavaScript 应用逻辑暂停下来,待垃圾回收完毕后再恢复。这种行为叫作“全停顿”(stop-the-world)。

    在 V8 新生代的分代回收中,只收集新生代,而新生代通常配置较小,且存活对象较少,所以全停顿的影响不大,而老生代就相反了。

    为了降低全部老生代全堆垃圾回收带来的停顿时间,V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直到标记阶段完成。

    经过增量标记改进后,垃圾回收的最大停顿时间可以减少到原来的 1/6 左右。

    五、内存泄漏

    内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

    六、内存泄漏的常见场景

    6.1 缓存

    文章前言部分就有说到,JS 开发者喜欢用对象的键值对来缓存函数的计算结果,但是缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。

    6.2 作用域未释放(闭包)

    var leakArray = [];
    exports.leak = function () {
    leakArray.push("leak" + Math.random());
    }

    以上代码,模块在编译执行后形成的作用域因为模块缓存的原因,不被释放,每次调用 leak 方法,都会导致局部变量 leakArray 不停增加且不被释放。

    闭包可以维持函数内部变量驻留内存,使其得不到释放。

    6.3 没必要的全局变量

    声明过多的全局变量,会导致变量常驻内存,要直到进程结束才能够释放内存。

    6.4 无效的 DOM 引用

    //dom still exist
    function click(){
    // 但是 button 变量的引用仍然在内存当中。
    const button = document.getElementById('button');
    button.click();
    }
    // 移除 button 元素
    function removeBtn(){
    document.body.removeChild(document.getElementById('button'));
    }
    
    

    6.5 定时器未清除

    // vue 的 mounted 或 react 的 componentDidMount
    componentDidMount() {
    setInterval(function () {
    // ...do something
    }, 1000)
    }

    vue 或 react 的页面生命周期初始化时,定义了定时器,但是在离开页面后,未清除定时器,就会导致内存泄漏。

    6.6 事件监听为清空

    componentDidMount() {
    window.addEventListener("scroll", function () {
    // do something...
    });
    }
    

    同 6.5, 在页面生命周期初始化时,绑定了事件监听器,但在离开页面后,未清除事件监听器,同样也会导致内存泄漏。

    七、内存泄漏优化

    1.解除引用

    确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)

    function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
    }
    var globalPerson = createPerson("Nicholas");
    // 手动解除 globalPerson 的引用
    globalPerson = null;
    

    解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

    2.提供手动清空变量的方法

    var leakArray = [];
    exports.clear = function () {
    leakArray = [];
    }

    3.在业务不需要用到的内部函数,可以重构在函数外,实现解除闭包
    4.避免创建过多生命周期较长的对象,或将对象分解成多个子对象

    5.避免过多使用闭包

    6.注意清除定时器和事件监听器

    7.Nodejs 中使用 stream 或 buffer 来操作大文件,不会受 Nodejs 内存限制

    8.使用 redis 等外部工具缓存数据

    总结

    JS 是一门具有自动垃圾收集的编程语言,在浏览器中主要通过标记清除方法来回收垃圾,NodeJs 中主要通过分代回收、Scavenge、标记清除、增量标记等算法来回收垃圾。在日常开发中,有一些不引人注意的书写方式可能会导致内存泄漏,需要多注意自己的代码规范。

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持北冥有鱼。

    您可能感兴趣的文章:

    • JavaScript中的垃圾回收与内存泄漏示例详解
    • 对于js垃圾回收机制的理解
    • 浅谈JavaScript 执行环境、作用域及垃圾回收
    • JS闭包、作用域链、垃圾回收、内存泄露相关知识小结
    • 跟我学习javascript的垃圾回收机制与内存管理
    • JavaScript 垃圾回收机制分析
    • js的新生代垃圾回收知识点总结

    广而告之:
    热门推荐:
    JavaScript 继承 封装 多态实现及原理详解

    面向对象的三大特性 封装 所谓封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的···

    用Php编写注册后Email激活验证的实例代码

    总共需两个页面,register.php 和 verify.php 1. 用户注册表格 register.php 复制代码 代码如下: <html>  <body>    <form action="register.php" method="post" name="register">       用户名:<···

    vue鼠标移入添加class样式,鼠标移出去除样式(active)实现方法

    鼠标移入添加class样式 HTML HTML绑定事件,加入或者移出class为active <div class="col-lg-3 col-xs-6 paddingLeft com_marginBtm10 choosePlan" v-on:mouseover="changeActive($event)" v-on:mouseout="removeActive($event)"> 流量套餐 </div> JS 这里除了ac···

    利用MySQL主从配置实现读写分离减轻数据库压力

    大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够。到了数据业务层、数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢失的话,后果更是 不堪设想。这时候,我们会考虑如何减少数据···

    为织梦自定义表单添加提交时间

    有不少用户都需要在织梦留言上面增加一个关于用户提交留言的时间,好区分用户什么时候提交的留言。现织梦模板之家技术钟振森给大家分享一个简单的增加留言时间。 首先自定义表单,然后添加字段, 比如联系人(单行文本),联系方式(单行文本),地址(单行文本)···

    php桌面中心(二) 数据库写入

    一、以下是数据库的写入的html程序,你可以加入密码功能。把密码做成变量发入下面那个写入的php程序。这样就实现了密码保护了:  <html>  <head>  <title>数据库</title>  <meta http-equiv="Content-Type" con···

    php上传apk后自动提取apk包信息的使用(示例下载)

    进入公司第一个项目就是做market市场。所以后台要上传APK软件之类。为了方便,上传APK后由系统自动提取APK文件的相关信息,比如:apk包名、产品名称、版本信息、APK Code、程序大小、ICON等。起初处理方式 通过命令:java -jar AXMLPrinter2.jar AndroidManifest.xml >···

    在Mac OS上搭建PHP的Yii框架及相关测试环境

    YII集成了单元测试和功能测试,借助phpunit和selenium实现。笔者在配置过程中遇到了不少麻烦,纪录在此。 必要概念 selenium selenium是个著名的自动化测试工具,可以调起本地的浏览器来完成测试,所以可以用来自动化测试web项目。selenium分为服务端和客户端,服务端使用java···

    Vue ElementUi同时校验多个表单(巧用new promise)

    前言 有ABCD四个表单,提交的时候同时校验(是的,后台管理系统的需求就是这样),巧用new promise。 实现的方法有很多种,我讲下自己觉得比较优雅的方式,欢迎各位大大的指正哈。 代码 let formArr=['formA','formB','formC','formD']//假设这是四个form表单的ref var resultAr···

    PHP之sprintf函数用法详解

    本文实例讲述了PHP中sprintf函数的用法。分享给大家供大家参考。具体用法分析如下: sprintf()函数在php官方是说把字符串格式化输出了,本文就来给各位朋友介绍一下在学习sprintf()函数时的一些经验分享,希望能给大家带来帮助. PHP函数 sprintf() 函数官方定义为:sprintf():把···