本文是 重温基础 系列文章的第二十二篇。
今日感觉:优化学习方法。 前端
系列目录:git
本章节复习的是JS中的内存管理,这对于咱们开发很是有帮助。 github
前置知识
绝大多数的程序语言,他们的内存生命周期基本一致: 算法
对于全部的编程语言,第二部分都是明确的。而第一和第三部分在底层语言中是明确的。
但在像JavaScript
这些高级语言中,大部分都是隐含的,由于JavaScript
具备自动垃圾回收机制(Garbage collected)。
所以在作JavaScript
开发时,不须要关心内存的使用问题,所需内存分配和无用内存回收,都彻底实现自动管理。编程
像C语言这样的高级语言通常都有底层的内存管理接口,好比malloc()
和free()
。另外一方面,JavaScript建立变量(对象,字符串等)时分配内存,而且在再也不使用它们时“自动”释放。 后一个过程称为 垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其余高级语言)开发者感受他们能够不关心内存管理。 这是错误的。
——《MDN JavaScript 内存管理》
MDN中的介绍告诉咱们,做为JavaScript
开发者,仍是须要去了解内存管理,虽然JavaScript
已经给咱们作好自动管理。segmentfault
在作JavaScript
开发时,咱们定义变量的时候,JavaScript
便为咱们完成了内存分配:数组
var num = 100; // 为数值变量分配内存 var str = 'pingan'; // 为字符串变量分配内存 var obj = { name : 'pingan' }; // 为对象变量及其包含的值分配内存 var arr = [1, null, 'hi']; // 为数组变量及其包含的值分配内存 function fun(num){ return num + 2; }; // 为函数(可调用的对象)分配内存 // 函数表达式也能分配一个对象 someElement.addEventListener('click', function(){ someElement.style.backgroundColor = 'blue'; }, false);
另外,经过调用函数,也会分配内存:浏览器
// 类型1. 分配对象内存 var date = new Date(); // 分配一个Date对象 var elem = document.createElement('div'); // 分配一个DOM元素 // 类型2. 分配新变量或者新对象 var str1 = "pingan"; var str2 = str1.substr(0, 3); // str2 是一个新的字符串 var arr1 = ["hi", "pingan"]; var arr2 = ["hi", "leo"]; var arr3 = arr1.concat(arr2); // arr3 是一个新的数组(arr1和arr2链接的结果)
使用内存的过程其实是对分配的内存进行读取与写入的操做。
一般表现就是使用定义的值。
读取与写入多是写入一个变量或者一个对象的属性值,甚至传递函数的参数。服务器
var num = 1; num ++; // 使用已经定义的变量,作递增操做
当咱们前面定义好的变量或函数(分配的内存)已经不须要使用的时候,便须要释放掉这些内存。这也是内存管理中最难的任务,由于咱们不知道何时这些内存不使用。
很好的是,在高级语言解释器中,已经嵌入“垃圾回收器”,用来跟踪内存的分配和使用,以便在内存不使用时自动释放(这并非百分百跟踪到,只是个近似过程)。微信
就像前面提到的,“垃圾回收器”只能解决通常状况,接下来咱们须要了解主要的垃圾回收算法和它们局限性。
垃圾回收算法主要依赖于引用的概念。
即在内存管理环境中,一个对象若是有权限访问另外一个对象,不论显式仍是隐式,称为一个对象引用另外一个对象。
例如:一个JS对象具备对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
注意:
这里的对象,不只包含JS对象,也包含函数做用域(或全局词法做用域)。
这个算法,把“对象是否再也不须要”定义为:当一个对象没有被其余对象所引用的时候,回收该对象。这是最初级的垃圾收集算法。
var obj = { leo : { age : 18 }; };
这里建立2个对象,一个做为leo
的属性被引用,另外一个被分配给变量obj
。
// 省略上面的代码 /* 咱们将前面的 { leo : { age : 18 }; }; 称为“这个对象” */ var obj2 = obj; // obj2变量是第二个对“这个对象”的引用 obj = 'pingan'; // 将“这个对象”的原始是引用obj换成obj2 var leo2 = obj2.leo; // 引用“这个对象”的leo属性
能够看出,如今的“这个对象”已经有2个引用,一个是obj2
,另外一个是leo2
。
obj2 = 'hi'; // 将obj2变成零引用,所以,obj2能够被垃圾回收 // 可是它的属性leo还在被leo2对象引用,因此还不能回收 leo2 = null; // 将leo变成零引用,这样obj2和leo2均可以被垃圾回收
这个算法有个限制:
没法处理循环引用。即两个对象建立时相互引用造成一个循环。
function fun(){ var obj1 = {}, obj2 = {}; obj1.leo = obj2; // obj1引用obj2 obj2.leo = obj1; // obj2引用obj1 return 'hi pingan'; } fun();
能够看出,它们被调用以后,会离开函数做用域,已经没有用了能够被回收,然而引用计数算法考虑到它们之间相互至少引用一次,因此它们不会被回收。
实际案例:
在IE6,7中,使用引用计数方式对DOM对象进行垃圾回收,经常形成对象被循环引用致使内存泄露:
var obj; window.onload = function(){ obj = document.getElementById('myId'); obj.leo = obj; obj.data = new Array(100000).join(''); };
能够看出,DOM元素obj
中的leo
属性引用了本身obj
,形成循环引用,若该属性(leo
)没有移除或设置为null
,垃圾回收器老是且至少有一个引用,并一直占用内存,即便从DOM树删除,若是这个DOM元素含大量数据(如data
属性)则会致使占用内存永远没法释放,出现内存泄露。
这个算法,将“对象是否再也不须要”定义为:对象是否能够得到。
标记清除算法,是假定设置一个根对象(root),在JS中是全局对象。垃圾回收器定时找全部从根开始引用的对象,而后再找这些对象引用的对象...直到找到全部能够得到的对象和搜集全部不能得到的对象。
它比引用计数垃圾收集更好,由于“有零引用的对象”老是不可得到的,可是相反却不必定,参考“循环引用”。
循环引用再也不是问题:
function fun(){ var obj1 = {}, obj2 = {}; obj1.leo = obj2; // obj1引用obj2 obj2.leo = obj1; // obj2引用obj1 return 'hi pingan'; } fun();
仍是这个代码,能够看出,使用标记清除算法来看,函数调用以后,两个对象没法从全局对象获取,所以将被回收。相同的,下面案例,一旦 obj
和其事件处理没法从根获取到,他们将会被垃圾回收器回收。
var obj; window.onload = function(){ obj = document.getElementById('myId'); obj.leo = obj; obj.data = new Array(100000).join(''); };
注意: 那些没法从根对象查询到的对象都将被清除。
在平常开发中,应该注意及时切断须要回收对象与根的联系,虽然标记清除算法已经足够强壮,就像下面代码:
var obj,ele=document.getElementById('myId'); obj.div = document.createElement('div'); ele.appendChild(obj.div); // 删除DOM元素 ele.removeChild(obj.div);
若是咱们只是作小型项目开发,JS用的比较少的话,内存管理能够不用太在乎,可是若是是大项目(SPA,服务器或桌面应用),那就须要考虑好内存管理问题了。
在计算机科学中,内存泄漏指因为疏忽或错误形成程序未能释放已经再也不使用的内存。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,致使在释放该段内存以前就失去了对该段内存的控制,从而形成了内存的浪费。 ——维基百科
其实简单理解:一些再也不使用的内存没法被释放。
当内存占用愈来愈多,不只影响系统性能,严重的还会致使进程奔溃。
未定义的变量,会被定义到全局,当页面关闭才会销毁,这样就形成内存泄露。以下:
function fun(){ name = 'pingan'; };
若是这里举一个定时器的案例,若是定时器没有回收,则不只整个定时器没法被内存回收,定时器函数的依赖也没法回收:
var data = {}; setInterval(function(){ var render = document.getElementById('myId'); if(render){ render.innderHTML = JSON.stringify(data); } }, 1000);
var str = null; var fun = function(){ var str2 = str; var unused = function(){ if(str2) console.log('is unused'); }; str = { my_str = new Array(100000).join('--'); my_fun = function(){ console.log('is my_fun'); }; }; }; setInterval(fun, 1000);
定时器中每次调用fun
,str
都会得到一个包含巨大的数组和一个对于新闭包my_fun
的对象,而且unused
是一个引用了str2
的闭包。
整个案例中,闭包之间共享做用域,尽管unused
可能一直没有调用,但my_fun
可能被调用,就会致使内存没法回收,内存增加致使泄露。
当咱们把DOM的引用保存在一个数组或Map中,即便移除了元素,但仍然有引用,致使没法回收内存。例如:
var ele = { img : document.getElementById('my_img') }; function fun(){ ele.img.src = "http://www.baidu.com/1.png"; }; function foo(){ document.body.removeChild(document.getElementById('my_img')); };
即便foo
方法将my_img
元素移除,但fun
仍有引用,没法回收。
经过Chrome浏览器查看内存占用:
步骤以下:
若是内存占用基本平稳,接近水平,就说明不存在内存泄漏。
反之,就是内存泄漏了。
命令行可使用 Node 提供的process.memoryUsage
方法。
console.log(process.memoryUsage()); // { rss: 27709440, // heapTotal: 5685248, // heapUsed: 3449392, // external: 8772 }
process.memoryUsage
返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义以下。
rss(resident set size)
:全部内存占用,包括指令区和堆栈。heapTotal
:"堆"占用的内存,包括用到的和没用到的。heapUsed
:用到的堆的部分。external
: V8 引擎内部的 C++ 对象占用的内存。判断内存泄漏,以heapUsed
字段为准。
本部份内容到这结束
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推荐 | https://github.com/pingan8787... |
JS小册 | js.pingan8787.com |
微信公众号 | 前端自习课 |