javascript的内存(垃圾)回收机制?
垃圾回收机制
1.js中的内存回收
在js中,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。
以全局变量和局部变量来说,函数中的局部变量在函数执行结束后这些变量已经不再被需要,所以垃圾回收器会识别并释放它们。
而对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以尽量少使用全局变量。
2.垃圾回收的两种模式
引用计数
引用计数的判断原理很简单,就是看一份数据是否还有指向它的引用,如果没有任何对象再指向它,那么垃圾回收器就会回收,举个例子:
// 创建一个对象,由变量o指向这个对象的两个属性 var o = { name: '听风是风', handsome: true }; // name虽然设置为了null,但o依旧有name属性的引用 o.name = null; var s = o; // 我们修改并释放了o对于对象的引用,但变量s依旧存在引用 o = null; // 变量s也不再引用,对象很快会被垃圾回收器释放 s = null;
引用计数存在一个很大的问题,就是对象间的循环引用,比如如下代码中,对象o1与o2相互引用,即便函数执行完毕,垃圾回收器通过引用计数也无法释放它们。
function f() { var o1 = {}; var o2 = {}; o1.a = o2; // o1 引用 o2 o2.a = o1; // o2 引用 o1 return; }; f();
标记清除
标记清除的概念也好理解,从根部出发看是否能达到某个对象,如果能达到则认定这个对象还被需要,如果无法达到,则释放它,这个过程大致分为三步:
a.垃圾回收器创建roots列表,roots通常是代码中保留引用的全局变量,在js中,我们一般认定全局对象window作为root,也就是所谓的根部。
b.从根部出发检查所有 的roots,所有的children也会被递归检查,能从root到达的都会被标记为active。
c.未被标记为active的数据被认定为不再需要,垃圾回收器开始释放它们。
当一个对象零引用时,我们从根部一定无法到达;但反过来,从根部无法到达的不一定是严格意义上的零引用,比如循环引用,所以标记清除要更优于引用计数。
从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法,但老版本的IE6除外。
如何避免内存泄漏
我们已经知道了垃圾回收的原理,那么我们如何避免创建无法回收的对象,以至造成内存泄漏的尴尬呢?下面说说常见的四种js内存泄漏。
1.全局变量
尽可能少的去创建全局变量是js开发者的常识,但如下两种方式还是会意外的创建全局变量,第一是在函数中声明变量未使用var:
function fn() { a = 1; }; fn(); window.a //1
上述代码中我们在函数体内声明了一个变量a,由于未使用var声明,即便在函数体内,但它依旧是一个全局变量。我们知道全局变量等同于在window上添加属性,所以在函数执行完毕,我们依旧可以访问到它。
第二种是在函数体内通过this来创建变量:
function fn() { this.a = 1; }; fn(); window.a //1
我们知道,当直接调用函数fn时,等同于window.fn(),所以函数体内的this会指向window,所以本质上还是创建了一个全局变量。
当然上述问题也不是无法解决,我们可以使用严格模式来避免这个问题,试着在代码头部添加‘use strict’,你会发现a就无法访问了,因为严格模式下,全局对象指向undefined。
有时候我们无法避免使用全局变量,那么记得在使用完毕后手动释放它们,例如让变量指向null。
2.被遗忘的定时器或回调函数
var serverData = loadData(); setInterval(function () { var renderer = document.getElementById('renderer'); if (renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 3000);
在上述代码中,当dom元素renderer被移除时,由于是周期定时器的缘故,定时器回调函数始终无法被回收,这也导致了定时器会一直对数据serverData保持引用,好的做法是在不需要时停止定时器。
在例如我们在使用事件监听时,如果不再需要监听记得移除监听事件。
var element = document.getElementById('button'); function onclick(event) { element.innerHTML = 'text'; }; element.addEventListener('click', onclick); // 移除监听 element.removeEventListener('click', onclick);
3.闭包
闭包在js开发中是极其常见的,我们来看个例子:
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { //unused未执行,但一直保持对theThing的引用 if (originalThing) console.log("hi"); }; //创建一个新对象 theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
定时器每次调用replaceThing,theThing都会获得一个包含数组longStr与闭包someMethod的新对象。
闭包unused保持着对象originalThing的引用,因为theThing赋值的缘故,也保持了对theThing的引用。
虽然unused没执行,但引用关系会导致originalThing一直无法被回收,那么theThing也一样。
正确做法是在replaceThing 最后添加originalThing = null;
所以我们常说,对于闭包中的变量,在不需要时一定记得手动释放。
4.DOM的引用
操作dom总是被认为是不好的,但一定得操作,我们的习惯是通过一个变量来存储它,这样就可以反复使用了,但这也会造成一个问题,dom会被引用2次。
var elements = document.getElementById('button') function doStuff() { elements.innerHTML = '听风是风'; }; // 清除引用 elements = null; document.body.removeChild(document.getElementById('button'));
在上述代码中,一次引用是基于dom树的引用,第二是变量elements的引用,当我们不需要这个dom时,都做两次清除操作。
补充:
不管是什么程序语言,内存的声明周期都满足以下三个阶段:
a.分配你需要的内存空间
b.使用分配到的内存(读、写)
c.不需要时将其释放或归还
JavaScript内存空间分为: 栈,堆,池,队列。
栈存放变量,基本类型数据与指向复杂类型数据的引用指针;
堆存放复杂类型数据;
池又称为常量池,用于存放常量;
而队列在任务队列也会使用。我们一一细说。
1.栈数据结构
栈数据结构具备FILO(first in last out)先进后出的特性,较为经典的就是乒乓球盒结构,先放进去的乒乓球只能最后取出来。
在js中数据类型一般分类基本数据类型(Number Boolean Null Undefined String Symbol)与引用数据类型(Object Array Function ...),其中栈一般用于存放基本类型数据,例如以下代码在栈内存中分布:
var a = 1; var b = a; a = 2;
可以看到基本类型数据的变量名与值都存放在栈内存中,当我们将变量a复制给b时,栈会新开内存用于存放变量b,且当我们修改变量a时对变量b不会造成任何影响,因为a与b是互不相关的两份数据。
2.堆数据结构
堆数据结构是一种无序的树状结构,同时它还满足key-value键值对的存储方式;我们只用知道key名,就能通过key查找到对应的value。比较经典的就是书架存书的例子,我们知道书名,就可以找到对应的书籍。
在js中堆内存一般用于存储引用类型的数据,需要注意的是由于引用类型的数据一般可以拓展,数据大小可变,所以存放在堆内存中;
但对引用类型数据的引用地址是固定的,所以地址指向还是会存放在栈内存中。
我们通过内存图来模拟以下代码:
var a = [1,2,3]; var b = a; a.push(4);
当我们创建数组a时,栈内存中只保存了变量a与指向堆内存中数组的地址指针,而当我们将a复制给变量b时,其实只是复制了一份地址指针,两者还是指向同一数组,无论谁修改,都会影响彼此。
这便是我们熟知的浅拷贝,若想对浅拷贝与深拷贝有更深了解。
3.队列
队列具有FIFO(First In First Out)先进先出的特性,与栈内存不同的是,栈内存只存在一个出口用于数据进栈出栈;而队列有一个入口与一个出口,理解队列一个较为实际的例子就像我们排队取餐,先排队的永远能先取到餐。
在js中使用队列较为突出的就是js执行机制中的event loop事件循环,如果大家对于js事件执行机制有兴趣。