JS中的内存管理,感觉就像JS中的一门副科,我们平时不太会忠实,但是一旦出现问题又很棘手,所以可以通过平时的多了解一些JS中的内存管理问题,在写代码中通过一些平时养成的习惯,避免内存泄露的问题。
不管什么语言,内存生命周期基本一致:
1、分配内存;
2、使用分配的内存(读、写);
3、不需要的时候再释放内存。
C语言中,有专门的内存管理接口,像malloc()和free()。JS中没有专门的内存管理接口,所有的内存管理都是自动的。JS创建变量是,自动分配内存,并在不使用的时候,自动释放该内存。这种自动的内存回收,造成了很多JS开发者并不关心内存回收问题,实际上,这会造成许多错误。
关于JS内存回收:
1、引用:
垃圾回收算法主要依赖于引用的概念,在内存管理环境中,一个对象如果有访问另一个对象的权限(隐式或显示),叫做一个对象引用另一个对象。例如:一个JavaScript对象具有对它模型的引用(隐式引用)和对它属性的引用(显示引用)
2、引用计数垃圾收集:
这是简单的垃圾收集算法,此算法吧“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用对象指向该对象,对象将被垃圾回收机制给回收。
示例:
let arr = [1,2,3];
arr = null;//[1,2,3]这时候没有被引用,会被自动回收
3:限制:循环使用:
在接下来的例子中,两个对象被创建并相互引用,就造成了循环引用。它们被调用之后不会离开函数作用域,所以它们已经没有用了,可以被回收了,然而,引用计数算法考虑到它们互相都有至少一次引用,所以不会被回收。
示例:
function f(){
var x = {};
var y = {};
x.p = y;//x引用y
y.p = x;//y引用x
//这里就形成了一个循环引用
}
f();
实际的例子:
var div;
window.onload = function(){
div = document.getElementById("myId");
div.circularReference = div;
div.lotsOfData = new Array(10000).join("*");
};
这个例子中,myId这个DOM元素里的circularReference属性引用了myId,造成了循环引用,IE6,7使用引用计数方式对DOM对象进行垃圾回收,该方式常常造成对象引用时内存发生泄漏。主流浏览器通过使用标记-清除 内存回收算法,来解决这一问题。
4:标记-清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法鉴定设置一个叫根root的对象(JS中,根是全局对象)。定期的,垃圾回收器将从根开始。找所有从根开始饮用的对象,然后找到这些对象引用的对面,从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。
从2012年起,所有的主流浏览器都使用了标记-清除内存回收算法。所有对JS垃圾回收算法的改进都基于该算法。
5:什么事内存泄露?
本质上讲,内存泄露就是不在被需要的内存,由于某种原因,无法被释放。
6:常见的内存泄露案例:
1:全局变量:
function foo(arg){
bar = "xx";
}
在JS中处理未被声明的变量,上述示例中的bar时,会把bar定义到全局对象中,在浏览器上也就是window上。在页面中的全局变量,只有当页面被关闭后才会被销毁。所以这种写法就会造成内存泄露,但让这个例子中泄露的只是一个简单的字符串,但是实际代码中,往往情况会更糟糕。
另外一种意外创建全局变量的情况:
function foo(){
this.varl = "xx";
}
foo();
foo被调用时,this指向全局变量window。意外的创建了全局变量,造成内存泄露。
我们谈到了一些意外情况下定义的全局变量,代码中也有一些我们明确定义的全局变量。如果使用这些全局变量来暂存大量的数据,记得在使用后,对其重新赋值为null。
2:未销毁的定时器和回调函数:
很多库中,如果使用了观察者模式,都会提供回调方法,来调用一些做后续处理的回调函数。要记得回收这些回调函数。
举一个setInterval的例子:
var serverData = loadData();
setInterval(function(){
var renderer = document.getElementById(‘renderer‘);
if(renderer){
renderer.innerHTML(JSON.stringify(serverData));
}
},5000);//5秒调用一次
如果后续renderer元素被溢出,整个定时器实际上没有任何作用,但是如果你不对其进行回收,整个定时器依然存在内存中,不但定时器无法被内存回收,定时器函数中的依赖也无法回收。在这个案例中的serverData也无法被回收。
3、闭包:
在JS开发中,我们会经常使用的闭包,一个内部函数,有权访问包含其的外部函数中的变量。
下面的这种情况,闭包也会造成内存泄露:
var theThing = null;
var replaceThing = function(){
var originalThing = theThing;
var unused = function(){
if(originalThing)//引用包含此函数的外部函数中的变量
console.log("x");
};
theThing = {
longStr:new Array(10000000).join(‘*‘),
someMethod:function(){
console.log("y");
}
};
};
setInterval(replaceThing,1000);
这段代码,每次调用replaceThing时,theThing获得了包含一个巨大的数组和一个对于新闭包someMethod的对象。同时unused是一个引用了originalThing的闭包。
这个范例的关键在于,闭包之间是共享作用域的,尽管unused可能一直没有被调用,但是someMethod可能会被调用,就会导致内存无法对其进行回收,这段代码被反复执行时,内存会持续的增长!
该问题的更多描述课件Meteor团队的文章!
4、DOM引用:
很多时候我们对DOM进行操作,会把DON的引用保存到一个数组或Map中
var elments = {
image:document.getElementById("image");
};
function doStuff(){
elements.image.src = ‘http://example.com/image_name.png‘;
}
function removeImage() {
document.body.removeChild(document.getElementById(‘image‘));
// 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}
上述案例中, 即使我们对于 image 元素进行了移除, 但是仍然有对 image 元素的引用, 依然无法对齐进行内存回收.
另外需要注意的一个点是, 对于一个 Dom 树的叶子节点的引用. 举个例子: 如果我们引用了一个表格中的 td 元素, 一旦在 Dom 中删除了整个表格, 我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素. 但是事实上, 这个 td 元素是整个表格的一个子元素, 并保留对于其父元素的引用. 这就会导致对于整个表格, 都无法进行内存回收. 所以我们要小心处理对于 Dom 元素的引用.
精读:
ES6中引入 WeakSet 和 WeakMap 两个新的概念, 来解决引用造成的内存回收问题. WeakSet 和 WeakMap 对于值的引用可以忽略不计, 他们对于值的引用是弱引用,内存回收机制, 不会考虑这种引用. 当其他引用被消除后, 引用就会从内存中被释放.
JS 这类高级语言,隐藏了内存管理功能。但无论开发人员是否注意,内存管理都在那,所有编程语言最终要与操作系统打交道,在内存大小固定的硬件上工作。不幸的是,即使不考虑垃圾回收对性能的影响,2017 年最新的垃圾回收算法,也无法智能回收所有极端的情况。
唯有程序员自己才知道何时进行垃圾回收,而 JS 由于没有暴露显示内存管理接口,导致触发垃圾回收的代码看起来像“垃圾”,或者优化垃圾回收的代码段看起来不优雅、甚至不可读。
所以在 JS 这类高级语言中,有必要掌握基础内存分配原理,在对内存敏感的场景,比如 nodejs 代码做严格检查与优化。谨慎使用 dom 操作、主动删除没有业务意义的变量、避免提前优化、过度优化,在保证代码可读性的前提下,利用性能监控工具,通过调用栈定位问题代码。
同时对于如何利用 chrome调试工具, 分析内存泄露的方法和技巧. 可以参考WEB前端教程-精读《2017前端性能优化备忘录》
总结:
即便在 JS 中, 我们很少去直接去做内存管理. 但是我们在写代码的时候, 也要有内存管理的意识, 谨慎的处理可能会造成内存泄露的场景.
原文链接:https://juejin.im/entry/59f9331551882546b15bd2bd?utm_source=gold_browser_extension