在js中创建一个变量时,会自动分配内存空间,当变量不再被使用时,垃圾回收机制会自动释放相应的内存空间。
如何判断一个变量不在被使用?方法有两种:
一、引用计数法:
引用计数的判断原理很简单,就是看一份数据是否还有指向它的引用,若是没有任何对象再指向它,那么垃圾回收器就会回收,其策略是跟踪记录每个变量值被使用的次数
-
当声明了一个变量并且将一个引用类型赋值给该变量时,这个值的引用次数就为 1
-
如果同一个值又被赋给另一个变量,那么引用数加 1
-
如果该变量的值被其他的值覆盖了,则引用次数减 1
-
当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
引用计数存存在一个致命的缺陷,当对象间存在循环引用时,引用次数始终不会为0,因此垃圾回收器不会释放它们。
function f() { var o1 = {}; var o2 = {}; o1.a = o2; // o1 引用 o2 o2.a = o1; // o2 引用 o1 return; };
var element = document.getElementById("some_element"); var myObject = new Object{); myObject. element = element; element.someObject = myObject;
这个例子在一个DOM元素(element)与一个原生 JavaScript对象(myobject)之间创建了循环引用。而想要解决循环引用,需要将引用地址置为 null
来切断变量与之前引用值的关系,当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
myObject.element = null; element,SomeObject = null;
二、标记清除法:
标记清除(Mark-Sweep),目前在 JavaScript引擎
里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎
都在采用标记清除算法。
此算法可以分为两个阶段,一个是标记阶段(mark),一个是清除阶段(sweep)。
标记阶段,垃圾回收器会从根对象开始遍历(在js中,通常认定全局对象window做为根)。每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象。
清除阶段,垃圾回收器会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作。
在标记阶段,从根对象1可以访问到B,从B又可以访问到E,那么B和E都是可到达对象,同样的道理,F、G、J和K都是可到达对象。
在回收阶段,所有未标记为可到达的对象都会被垃圾回收器回收。
标记清除法会导致内存碎片化。由于空闲内存块是不连续的,容易出现很多空闲内存块,假设我们新建对象分配内存时需要大小为 size
,由于空闲内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于 size
的块才能为其分配(如下图)
那如何找到合适的块呢?我们可以采取下面三种分配策略
-
First-fit
,找到大于等于size
的块立即返回 -
Best-fit
,遍历整个空闲列表,返回大于等于size
的最小分块 -
Worst-fit
,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size
大小,并将该部分返回
这三种策略里面 Worst-fit
的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,对于 First-fit
和 Best-fit
来说,考虑到分配的速度和效率 First-fit
是更为明智的选择,但即便是使用 First-fit
策略,其操作仍是一个 O(n)
的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
三、v8引擎的垃圾回收机制
Chrome 浏览器所使用的 V8 引擎采用的分代回收策略,该策略通过区分「临时」与「持久」对象;多回收「临时对象区」(young generation),少回收「持久对象区」(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。
「临时」与「持久」对象也被叫做作「新生代」与「老生代」对象。
1. 新生代的特点:
- 通常把小的对象分配到新生代
- 新生代的垃圾回收比较频繁
- 通常存储容量在1~8M
2. 新生代-Scavenge算法
该算法将新生代分为两部分,一部分叫做from(对象区域),另一部分叫做to(空闲区域),新加入的对象首先存放在from区域;
from区域写满的时候,对from区域开始进行垃圾回收。首先对from区域的垃圾进行标记(红色代表标记为垃圾);
将存活的对象复制到to区域中,并且有序地排列起来,复制后的to区域就没有内存碎片了;
清空from区域;
from区域和to区域进行反转,也就是原来的from区域变为to区域,原来的to区域变成from区域。
Scavenge算法在时间效率上有着优异的表现,缺点是只能使用堆内存中的一半,如果存储容量过大,就会导致每次清理的时间过长,效率低,因此经过两次垃圾回收之后依然存活的对象会晋升为老生代对象,另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的原因是,当完成 Scavenge
回收后,空闲区将翻转成对象区域,继续进行对象内存的分配,若占比过大,将会影响后续内存分配。
3. 老生代的特点:
- 对象占用空间大
- 对象存活时间长
4. 老生代-标记整理法
- 标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象;
- 整理:让所有存活的对象都向内存的一端移动
5. 何时执行垃圾回收?
由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法
四、哪些情况容易引起内存泄漏
1. 全局变量
全局变量等同于在window上添加属性,因此在函数执行完毕,依旧能够访问到它,因此不能够被回收。
2. 闭包
3. 被遗忘的定时器或事件回调函数
当dom元素被移除时,因为是周期定时器的缘故,定时器回调函数始终没法被回收,这也致使了定时器会一直对数据serverData保持引用,好的作法是在不须要时中止定时器
var serverData = loadData(); setInterval(function () { var dom = document.getElementById('renderer'); if (dom) { dom.innerHTML = JSON.stringify(serverData); } }, 3000);
另外在使用事件监听时,若是再也不须要监听记得移除监听事件
var element = document.getElementById('button'); function onclick(event) { element.innerHTML = 'text'; }; element.addEventListener('click', onclick); // 移除监听 element.removeEventListener('click', onclick);