深入理解 JS 垃圾回收


Posted in Javascript onJune 03, 2019

前言

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 中,将内存分为新生代和老生代,新生代的对象为存活时间较短的对象,老生代的对象为存活事件较长或常驻内存的对象。

深入理解 JS 垃圾回收

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

4.2 Scavenge 算法

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

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

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

深入理解 JS 垃圾回收

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

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

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

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

4.4 增量标记(Incremental Marking)算法

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

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

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

深入理解 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 相关文章推荐
经常用的图片在容器中的水平垂直居中实例
Jun 10 Javascript
理解JavaScript的prototype属性
Feb 11 Javascript
基于jQuery实现以手风琴方式展开和折叠导航菜单
Jan 28 Javascript
WEB前端实现裁剪上传图片功能
Oct 17 Javascript
Dropzone.js实现文件拖拽上传功能(附源码下载)
Nov 22 Javascript
Angular4实现动态添加删除表单输入框功能
Aug 11 Javascript
简单谈谈js的数据类型
Sep 25 Javascript
vue2.0实现音乐/视频播放进度条组件
Jun 06 Javascript
JS如何定义用字符串拼接的变量
Jul 11 Javascript
解决ant Design中this.props.form.validateFields未执行的问题
Oct 27 Javascript
在VUE中使用lodash的debounce和throttle操作
Nov 09 Javascript
vue项目实现分页效果
Mar 24 Vue.js
如何让微信小程序页面之间的通信不再变困难
Jun 03 #Javascript
使用VueRouter的addRoutes方法实现动态添加用户的权限路由
Jun 03 #Javascript
使用watch在微信小程序中实现全局状态共享
Jun 03 #Javascript
深入理解JS异步编程-Promise
Jun 03 #Javascript
模块化react-router配置方法详解
Jun 03 #Javascript
react 组件传值的三种方法
Jun 03 #Javascript
angular使用md5,CryptoJS des加密的方法
Jun 03 #Javascript
You might like
在PHP的图形函数中显示汉字
2006/10/09 PHP
GBK的页面输出JSON格式的php函数
2010/02/16 PHP
如何设置mysql允许外网访问
2013/06/04 PHP
PHP简单实现HTTP和HTTPS跨域共享session解决办法
2015/05/27 PHP
老生常谈PHP面向对象之标识映射
2017/06/21 PHP
可实现多表单提交的javascript函数
2007/08/01 Javascript
JS Array对象入门分析
2008/10/30 Javascript
jQuery on()方法绑定动态元素的点击事件无响应的解决办法
2016/07/07 Javascript
js for循环倒序输出数组元素的实例
2017/03/01 Javascript
AngularJS 教程及实例代码
2017/10/23 Javascript
vue+vuecli+webpack中使用mockjs模拟后端数据的示例
2017/10/24 Javascript
AngularJS遍历获取数组元素的方法示例
2017/11/11 Javascript
vue中使用ueditor富文本编辑器
2018/02/08 Javascript
详解Angular5 路由传参的3种方法
2018/04/28 Javascript
Node.js的Koa实现JWT用户认证方法
2018/05/05 Javascript
Taro集成Redux快速上手的方法示例
2018/06/21 Javascript
详解微信小程序-获取用户session_key,openid,unionid - 后端为nodejs
2019/04/29 NodeJs
JavaScript命名空间模式实例详解
2019/06/20 Javascript
微信小程序的授权实现过程解析
2019/08/02 Javascript
微信小程序模板消息限制实现无限制主动推送的示例代码
2019/08/27 Javascript
浅析Vue 中的 render 函数
2020/02/28 Javascript
JS中的const命令你真懂它吗
2020/03/08 Javascript
vue+springboot+element+vue-resource实现文件上传教程
2020/10/21 Javascript
解决element-ui的下拉框有值却无法选中的情况
2020/11/07 Javascript
Python实现的调用C语言函数功能简单实例
2019/03/13 Python
python实现对象列表根据某个属性排序的方法详解
2019/06/11 Python
使用python实现简单五子棋游戏
2019/06/18 Python
Python代码注释规范代码实例解析
2020/08/14 Python
Python钉钉报警及Zabbix集成钉钉报警的示例代码
2020/08/17 Python
为什么要有struct关键字
2012/05/08 面试题
车工岗位职责
2013/11/26 职场文书
文明村镇申报材料
2014/05/06 职场文书
运动会广播稿50字
2015/08/19 职场文书
Nginx解决403 forbidden的完整步骤
2021/04/01 Servers
Mysql实现主从配置和多主多从配置
2021/06/02 MySQL
MySQL事务的ACID特性以及并发问题方案
2022/07/15 MySQL