JavaScript中的垃圾回收与内存泄漏示例详解


Posted in Javascript onMay 02, 2019

前言

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存。所谓的内存泄漏简单来说是不再用到的内存,没有及时释放。为了更好避免内存泄漏,我们先介绍Javascript垃圾回收机制。

在C与C++等语言中,开发人员可以直接控制内存的申请和回收。但是在Java、C#、JavaScript语言中,变量的内存空间的申请和释放都由程序自己处理,开发人员不需要关心。也就是说Javascript具有自动垃圾回收机制(Garbage Collecation)。

一、垃圾回收的必要性

下面这段话引自《JavaScript权威指南(第四版)》

  由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

JavaScript中的垃圾回收与内存泄漏示例详解

这段话解释了为什么需要系统需要垃圾回收,JavaScript不像C/C++,它有自己的一套垃圾回收机制。

JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

var a = "浪里行舟";
var b = "前端工匠";
var a = b; //重写a

这段代码运行之后,“浪里行舟”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。

二、垃圾回收机制

垃圾回收机制怎么知道,哪些内存不再需要呢?

垃圾回收有两种方法:标记清除、引用计数。引用计数不太常用,标记清除较为常用。

1.标记清除

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

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

JavaScript中的垃圾回收与内存泄漏示例详解

我们用个例子,解释下这个方法:

var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
 a++
 var c = a + b
 return c
}

2.引用计数

所谓"引用计数"是指语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

JavaScript中的垃圾回收与内存泄漏示例详解

上图中,左下角的两个值,没有任何引用,所以可以释放。

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。

var arr = [1, 2, 3, 4];
arr = [2, 4, 5]
console.log('浪里行舟');

上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存。至于如何释放内存,我们下文介绍。

第三行代码中,数组[1, 2, 3, 4]引用的变量arr又取得了另外一个值,则数组[1, 2, 3, 4]的引用次数就减1,此时它引用次数变成0,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。

但是引用计数有个最大的问题: 循环引用

function func() {
 let obj1 = {};
 let obj2 = {};

 obj1.a = obj2; // obj1 引用 obj2
 obj2.a = obj1; // obj2 引用 obj1
}

当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。

要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:

obj1 = null;
obj2 = null;

三、哪些情况会引起内存泄漏?

虽然JavaScript会自动垃圾收集,但是如果我们的代码写法不当,会让变量一直处于“进入环境”的状态,无法被回收。下面列一下内存泄漏常见的几种情况:

1.意外的全局变量

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

bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。

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

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

在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。

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

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

这样的代码很常见,如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。

3.闭包

function bindEvent(){
 var obj=document.createElement('xxx')
 obj.onclick=function(){
 // Even if it is a empty function
 }
}

闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调引用外部函数,形成了闭包。

// 将事件处理函数定义在外面
function bindEvent() {
 var obj = document.createElement('xxx')
 obj.onclick = onclickHandler
}
// 或者在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
 var obj = document.createElement('xxx')
 obj.onclick = function() {
 // Even if it is a empty function
 }
 obj = null
}

解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。

4.没有清理的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() {
 document.body.removeChild(document.getElementById('button'));
 // 此时,仍旧存在一个全局的 #button 的引用
 // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}

虽然我们用removeChild移除了button,但是还在elements对象里保存着#button的引用,换言之,DOM元素还在内存里面。

四、内存泄漏的识别方法

新版本的chrome在 performance 中查看:

JavaScript中的垃圾回收与内存泄漏示例详解

步骤:

  • 打开开发者工具 Performance
  • 勾选 Screenshots 和 memory
  • 左上角小圆点开始录制(record)
  • 停止录制

图中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为min),min在不断上涨,那么肯定是有较为严重的内存泄漏问题。

避免内存泄漏的一些方式:

  • 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收
  • 注意程序逻辑,避免“死循环”之类的
  • 避免创建过多的对象

总而言之需要遵循一条原则:不用了的东西要及时归还

五、垃圾回收的使用场景优化

1.数组array优化

将[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。

const arr = [1, 2, 3, 4];
console.log('浪里行舟');
arr.length = 0 // 可以直接让数字清空,而且数组类型不变。
// arr = []; 虽然让a变量成一个空数组,但是在堆上重新申请了一个空数组对象。

2. 对象尽量复用

对象尽量复用,尤其是在循环等地方出现创建新对象,能复用就复用。不用的对象,尽可能设置为null,尽快被垃圾回收掉。

var t = {} // 每次循环都会创建一个新对象。
for (var i = 0; i < 10; i++) {
 // var t = {};// 每次循环都会创建一个新对象。
 t.age = 19
 t.name = '123'
 t.index = i
 console.log(t)
}
t = null //对象如果已经不用了,那就立即设置为null;等待垃圾回收。

3.在循环中的函数表达式,能复用最好放到循环外面。

// 在循环中最好也别使用函数表达式。
for (var k = 0; k < 10; k++) {
 var t = function(a) {
 // 创建了10次 函数对象。
 console.log(a)
 }
 t(k)
}
// 推荐用法
function t(a) {
 console.log(a)
}
for (var k = 0; k < 10; k++) {
 t(k)
}
t = null

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
跨域表单提交状态的变相判断代码
Nov 12 Javascript
jQuery使用数组编写图片无缝向左滚动
Dec 11 Javascript
快速解决jquery.touchSwipe左右滑动和垂直滚动条冲突
Apr 15 Javascript
jquery组件WebUploader文件上传用法详解
Oct 23 Javascript
基于Bootstrap的网页设计实例
Mar 01 Javascript
基于javascript的异步编程实例详解
Apr 10 Javascript
JavaScript实现树的遍历算法示例【广度优先与深度优先】
Oct 26 Javascript
vue+element-ui+ajax实现一个表格的实例
Mar 09 Javascript
jQuery实现点击自身以外区域关闭弹出层功能完整示例【改进版】
Jul 31 jQuery
10行代码实现微信小程序滑动tab切换
Dec 28 Javascript
微信小程序webview实现长按点击识别二维码功能示例
Jan 24 Javascript
微信小程序通过js实现瀑布流布局详解
Aug 28 Javascript
详解微信小程序缓存--缓存时效性
May 02 #Javascript
详解如何使用router-link对象方式传递参数?
May 02 #Javascript
详解Vue底部导航栏组件
May 02 #Javascript
微信小程序搭建自己的Https服务器
May 02 #Javascript
Node.js中Koa2在控制台输出请求日志的方法示例
May 02 #Javascript
详解微信小程序网络请求接口封装实例
May 02 #Javascript
vue 搭建后台系统模块化开发详解
May 01 #Javascript
You might like
ThinkPHP的MVC开发机制实例解析
2014/08/23 PHP
php定时执行任务设置详解
2015/02/06 PHP
PHP函数用法详解【初始化、嵌套、内置函数等】
2020/06/02 PHP
jQuery 打造动态下滑菜单实现说明
2010/04/15 Javascript
jQuery中extend()和fn.extend()方法详解
2015/06/03 Javascript
jquery实现Li滚动时滚动条自动添加样式的方法
2015/08/10 Javascript
使用postMesssage()实现跨域iframe页面间的信息传递方法
2016/03/29 Javascript
总结在前端排序中遇到的问题
2016/07/19 Javascript
AngularJS基础 ng-include 指令示例讲解
2016/08/01 Javascript
AngularJS基础 ng-switch 指令简单示例
2016/08/03 Javascript
微信js-sdk界面操作接口用法示例
2016/10/12 Javascript
Bootstrap学习笔记之进度条、媒体对象实例详解
2017/03/09 Javascript
js使用html2canvas实现屏幕截取的示例代码
2017/08/28 Javascript
Vue刷新修改页面中数据的方法
2018/09/16 Javascript
mpvue实现微信小程序快递单号查询代码
2020/04/03 Javascript
vue 限制input只能输入正数的操作
2020/08/05 Javascript
探究Python中isalnum()方法的使用
2015/05/18 Python
Python多线程实现同步的四种方式
2017/05/02 Python
Python OpenCV 使用滑动条来调整函数参数的方法
2019/07/08 Python
ipad上运行python的方法步骤
2019/10/12 Python
python常用数据重复项处理方法
2019/11/22 Python
python实现拉普拉斯特征图降维示例
2019/11/25 Python
python如何实现单链表的反转
2020/02/10 Python
python+adb命令实现自动刷视频脚本案例
2020/04/23 Python
世界上最大的罕见唱片、CD和音乐纪念品网上商店:991.com
2018/05/03 全球购物
No7 Beauty美国官网:英国国民护肤品牌
2019/10/31 全球购物
大学专科生推荐信范文
2013/11/23 职场文书
档案管理员岗位职责
2013/12/01 职场文书
大学英语演讲稿范文
2014/04/24 职场文书
教室标语大全
2014/06/21 职场文书
小学校园广播稿(3篇)
2014/09/19 职场文书
2015年师德师风自我评价范文
2015/03/05 职场文书
放假通知格式
2015/04/14 职场文书
企业投资意向书
2015/05/09 职场文书
MySQL 重写查询语句的三种策略
2021/05/10 MySQL
VS2019连接MySQL数据库的过程及常见问题总结
2021/11/27 MySQL