简介

在c语言中,咱们须要手动调配和开释对象的内存,然而在java中,所有的内存治理都交给了java虚拟机,程序员不须要在手动过程内存的调配和开释,大大的缩小了程序编写的难度。

同样的,在javascript中,内存治理也是主动进行的,尽管有主动的内存治理措施,然而这并不意味着程序员就不须要关怀内存治理了。

本文将会进行具体的介绍javascript中的内存管理策略。

内存生命周期

对于任何程序来说,内存的生命周期通常都是一样的。

能够分为三步:

  1. 在可用空间分配内存
  2. 应用该内存空间
  3. 在应用结束之后,开释该内存空间

所有的程序都须要手动执行第二步,对于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-d0f7ebceab5aFor 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的博客

欢送关注我的公众号:「程序那些事」最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!