垃圾收集机制的原理是什么呢?其实就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。
js内存生命周期
申请内存空间: let obj = {};
使用内存空间: (读写操作) obj.name = ‘sunny’;
释放内存空间: (js中并没有相应的释放api) obj = null;
js中的垃圾回收
js中的内存管理是自动的,每当我们创建函数、对象、数组的时候会自动的分配相应的内存空间;
对象不再被引用的时候是垃圾;
对象不能从根上访问到时也是垃圾;
垃圾回收机制的两种策略
1、标记清除法
实现原理
核心思想就是将整个垃圾回收操作分为两个阶段:
- 遍历所有的对象找到活动对象,进行标记的操作
- 遍历所有的对象,找到那些没有标记的对象进行清除。(注意在第二阶段中也会把第一阶段涉及的标志给抹掉,便于GC下次能够正常的工作)
通过两次的遍历行为把我们当前的垃圾空间进行回收,最终交给我们的空闲列表进行维护。
总结
核心思想:分标记和清除两个阶段:
- 遍历所有的对象找活动对象做标记
- 遍历所有的对象清除没有标记的对象
- 回收相应空间
优点
可以回收循环引用的对象空间。相对于引用计数算法来说:解决对象循环引用的不能回收问题。
缺点
容易产生碎片化空间,浪费空间、不能立即回收垃圾对象。
空间碎片化:所谓空间碎片化就是由于当前所回收的垃圾对象,在地址上面是不连续的,由于这种不连续造成了我们在回收之后分散在各个角落,造成后续使用的问题
2、引用计数
实现原理
内部通过引用计数器,来维护当前对象的引用数,从而判断该对象的引用数是否为0,来决定它是否是一个垃圾对象。如果引用数值为0,GC就开始工作,将其所在的内存空间进行回收释放和再使用
引用计数器
当某一个对象的引用关系发生改变时,引用计数器就会自动的修改这个对象所对应的引用数值(比如我们代码中有一个对象空间,一个变量引用了它,那么这个对象空间的引用数值加1) 引用数值为0的时候GC就开始工作,将当前的对象空间回收
引用计数的优点
可以即时回收垃圾对象、减少程序卡顿时间。
- 发现垃圾立即回收(因为根据当前的引用数值是否为0判断他是否是一个垃圾,如果是垃圾就回收内存空间,释放内存)
- 最大限度的减少程序暂停(应用程序在执行的过程中必定会对内存进行消耗,而当前的执行平台内存空间是有上限的,所以内存肯定会有占满的时候。由于引用计数算法时刻监控着那些引用数值为0的对象,当内存爆满的时候会去找那些引用数值为0的对象释放其内存,这个也就保证了当前的内存空间不会有占满的时候)
引用计数的缺点
无法回收循环引用的对象、资源消耗较大
无法回收循环引用的对象
时间开销大(当前的引用计数需要去维护一个数值的变化,时刻监控当前引用数值是否修改,修改需要时间)
内存泄露
能导致内存泄漏的一定是引用类型的变量,比如函数和其他自定义对象。而值类型的变量是不存在内存泄漏的,比如字符串,数字,布尔值等。
当我们JacaScript代码创建一个引用类型的时候(以下简称对象),js引擎会在内存中开辟一块空间来存放数据,并把指针引用交给那个变量,内存是有限的,js引擎必须保证当开辟的对象没用的时候,把所分配的内存空间释放出来,这个过程叫做垃圾回收,负责回收的叫做垃圾回收器(GC)
内存泄漏是指我们已经无法再通过js代码来引用到某个对象,但垃圾回收器却认为这个对象还在被引用,因此在回收的时候不会释放它。导致了分配的这块内存永远也无法被释放出来。如果这样的情况越来越多,会导致内存不够用而系统崩溃。
造成内存泄漏的主要原因与解决办法
1、意外的全局变量
未定义的变量会在全局对象创建一个新的变量,如下:
//函数foo内部忘记使用var,实际上JS会把bar挂载到全局对象上,意外创建一个全局变量
function foo(){
bar = "hello world"
}
另一个意外的全局变量可能是由this创建
//Foo 调用自己,this 指向了全局对象(window)
// 而不是 undefined foo();
function foo(){
this.variable = "hello world"
}
/**
解决办法:在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。
*/
2、被遗忘的计时器或回调函数
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) { // 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
上面的例子表明,在节点node或者数据不再需要时,定时器依旧指向性这些数据,所以哪怕当node节点被移除后,interval依旧存活并且垃圾回收器没办法回收,他的依赖也没办法被回收,除非终止定时器。
3、脱离DOM的引用
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML); // 更多逻辑
}
function removeButton() { // 按钮是 body 的后代元素
document.body.removeChild(document.getElementById('button'));
// 此时,仍旧存在一个全局的 #button 的引用
// elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}
闭包真是造成内存泄漏的原因之一吗?
不是!
闭包只是其内部函数记住并访问了所在的词法作用域(以chrome的说法),也会产生较大的内存占用,但是,和内存泄漏有关系吗?看着一些系列文总结说与内存泄漏有关系你们就觉得与内存泄漏有关系呀?硬说有关系那是上世纪某个版本IE的bug了。
如何排查内存泄漏
阮老师有详细记录
最后
为了预防内存泄露及持有不必要的内存,应记得在函数结尾或适宜的时候对不需要引用的对象进行释放。为了确保有效地回收内存,应及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。