共计 4600 个字符,预计需要花费 12 分钟才能阅读完成。
简介
在 c 语言中,咱们须要手动调配和开释对象的内存,然而在 java 中,所有的内存治理都交给了 java 虚拟机,程序员不须要在手动过程内存的调配和开释,大大的缩小了程序编写的难度。
同样的,在 javascript 中,内存治理也是主动进行的,尽管有主动的内存治理措施,然而这并不意味着程序员就不须要关怀内存治理了。
本文将会进行具体的介绍 javascript 中的内存管理策略。
内存生命周期
对于任何程序来说,内存的生命周期通常都是一样的。
能够分为三步:
- 在可用空间分配内存
- 应用该内存空间
- 在应用结束之后,开释该内存空间
所有的程序都须要手动执行第二步,对于 javascript 来说,第 1,3 两步是隐式实现的。
咱们看下 javascript 中分配内存空间的例子。
通过初始化分配内存空间:
var n = 123; // 为数字分配内存
var s = 'azerty'; // 为 String 分配内存
var o = {
a: 1,
b: null
}; // 为对象分配内存
// 为数组分配内存
var a = [1, null, 'abra'];
function f(a) {return a + 2;} // 为函数分配内存
通过函数调用分配内存空间:
var d = new Date(); // 通过 new 调配 date 对象
var e = document.createElement('div'); // 调配一个 DOM 对象
var s = 'azerty';
var s2 = s.substr(0, 3); // 因为 js 中字符串是不可变的,所以 substr 的操作将会创立新的字符串
var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2);
// 同样的,concat 操作也会创立新的字符串
开释空间最难的局部就是须要判断空间什么时候不再被应用。在 javascript 中这个操作是由 GC 垃圾回收器来执行的。
垃圾回收器的作用就是在对象不再被应用的时候进行回收。
JS 中的垃圾回收器
判断一个对象是否能够被回收的一个十分重要的规范就是援用。
如果一个对象被另外一个对象所援用,那么这个对象必定是不可能被回收的。
援用计数垃圾回收算法
援用计数垃圾回收算法是一种比较简单和简洁的垃圾回收算法。他把对象是否可能被回收转换成了对象是否依然被其余对象所援用。
如果对象没有被援用,那么这个对象就是能够被垃圾回收的。
咱们举一个援用计数的例子:
var x = {
a: {b: 2}
};
// 咱们创立了两个对象,a 对象和 a 里面用大括号创立的对象。// 咱们将大括号创立的对象援用赋值给了 x 变量,所以 x 领有大括号创建对象的援用,该对象不可能被回收。// 同时,因为 a 对象是创立在大括号对象外部的,所以大括号对象默认领有 a 对象的援用
// 因为两个对象都有援用,所以都不可能被垃圾回收
var y = x; // 咱们将 x 赋值给 y,大括号对象当初领有两个援用
x = 1; // 咱们将 1 赋值给 x,这样只有 y 援用了大括号的对象
var z = y.a; // 将 y 中的 a 对象援用赋值给 z,a 对象领有两个援用
y = 'flydean'; // 从新赋值给 y,大括号对象的援用数为 0,大括号对象能够被回收了,然而因为其外部的 a 对象还有一个 z 在被援用
// 所以临时不能被回收
z = null; // z 援用也被从新赋值,a 对象的援用数为 0,两个对象都能够被回收了
援用计数的一个毛病就是可能会呈现循环援用的状况。
思考上面的一个例子:
function f() {var x = {};
var y = {};
x.a = y; // x references y
y.a = x; // y references x
return 'flydean';
}
f();
在下面的例子中,x 中的 a 属性援用了 y。而 y 中的 a 属性又援用了 x。
从而导致循环援用的状况,最终导致内存泄露。
在理论的利用中,IE6 和 IE7 对 DOM 对象应用的就是援用计数的垃圾回收算法,所以可能会呈现内存泄露的状况。
var div;
window.onload = function() {div = document.getElementById('myDivElement');
div.circularReference = div;
div.lotsOfData = new Array(10000).join('*');
};
下面的例子中,DOM 中的 myDivElement 元素应用 circularReference 援用了他自身,如果在援用计数的状况下,myDivElement 是不会被回收的。
当 myDivElement 中蕴含了大量的数据的时候,即便 myDivElement 从 DOM tree 中删除了,myDivElement 也不会被垃圾回收,从而导致内存泄露。
Mark-and-sweep 回收算法
讲到这里,大家是不是感觉 JS 的垃圾回收算法和 java 中的很相似,java 中也有援用计数和 mark-and-sweep 革除算法。
这种回收算法的判断规范是对象不可达。
在 javascript 中,通过扫描 root 对象(JS 中的 root 对象那些全局对象),而后找到这些 root 对象的援用对象,而后再找到这些被援用对象的援用对象,一层一层的往后查找。
最初垃圾回收器会找到所有的可达的对象和不可达的对象。
应用不可达来标记不再被应用的对象能够无效的解决援用计数法中呈现的循环援用的问题。
事实上,当初基本上所有的古代浏览器都反对 Mark-and-sweep 回收算法。
调试内存问题
如果发送了内存泄露,咱们该怎么调试和发现这个问题呢?
在 nodejs 中咱们能够增加 –inspect,而后借助 Chrome Debugger 来实现这个工作:
node --expose-gc --inspect index.js
下面的代码将会开启 nodejs 的调试性能。
咱们看下输入后果:
Debugger listening on ws://127.0.0.1:9229/88c23ae3-9081-41cd-98b0-d0f7ebceab5a
For help, see: https://nodejs.org/en/docs/inspector
后果通知了咱们两件事件,第一件事件就是 debugger 监听的端口。默认状况下将会开启 127.0.0.1 的 9229 端口。并且调配了一个惟一的 UUID 以供辨别。
第二件事件就是通知咱们 nodejs 应用的调试器是 Inspector。
应用 Chrome devTools 进行调试的前提是咱们曾经开启了 –inspect 模式。
在 chrome 中输出 chrome://inspect:
咱们可看到 chrome inspect 的界面,如果你本地曾经有开启 inspect 的 nodejs 程序的话,在 Remote Target 中就能够间接看到。
选中你要调试的 target,点击 inspect,即可开启 Chrome devTools 调试工具:
你能够对程序进行 profile,也能够进行调试。
闭包 Closures 中的内存泄露
所谓闭包就是指函数中的函数,外部函数能够拜访内部函数的参数或者变量,从而导致内部函数外部变量的援用。
咱们看一个简略闭包的例子:
function parentFunction(paramA)
{
var a = paramA;
function childFunction()
{return a + 2;}
return childFunction();}
下面的例子中,childFunction 援用了 parentFunction 的变量 a。只有 childFunction 还在被应用,a 就无奈被开释,从而导致 parentFunction 无奈被垃圾回收。事实上 Closure 默认就蕴含了对父 function 的援用。
咱们看上面的例子:
<html>
<body>
<script type="text/javascript">
document.write("Program to illustrate memory leak via closure");
window.onload=function outerFunction(){var obj = document.getElementById("element");
obj.onclick=function innerFunction(){alert("Hi! I will leak");
};
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
// This is used to make the leak significant
};
</script>
<button id="element">Click Me</button>
</body>
</html>
下面的例子中,obj 援用了 DOM 对象 element,而 element 的 onclick 是 outerFunction 的外部函数,从而导致了对外部函数的援用,从而援用了 obj。
这样最终导致循环援用,造成内存泄露。
怎么解决这个问题呢?
一个简略的方法就是在应用完 obj 之后,将其赋值为 null,从而中断循环援用的关系:
<html>
<body>
<script type="text/javascript">
document.write("Avoiding memory leak via closure by breaking the circular
reference");
window.onload=function outerFunction(){var obj = document.getElementById("element");
obj.onclick=function innerFunction()
{alert("Hi! I have avoided the leak");
// Some logic here
};
obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
obj = null; //This breaks the circular reference
};
</script>
<button id="element">"Click Here"</button>
</body>
</html>
还有一种很简洁的方法就是不要应用闭包,将其分成两个独立的函数:
<html>
<head>
<script type="text/javascript">
document.write("Avoid leaks by avoiding closures!");
window.onload=function()
{var obj = document.getElementById("element");
obj.onclick = doesNotLeak;
}
function doesNotLeak()
{
//Your Logic here
alert("Hi! I have avoided the leak");
}
</script>
</head>
<body>
<button id="element">"Click Here"</button>
</body>
</html>
本文作者:flydean 程序那些事
本文链接:http://www.flydean.com/js-memory-management/
本文起源:flydean 的博客
欢送关注我的公众号:「程序那些事」最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!