D012-垃圾回收机制及内存管理
内存的生命周期:可以分为3个过程创建,使用,销毁
内存的创建:(声明一个变量,对象,函数,数组,new一个构造函数等--- 都会在内存上开辟一个空间 内存的使用: 对于变量,对象等的一些操作,比如:赋值,累加求和,获取dom元素,并修改属性等等 内存的回收:不再使用的变量,会被js引擎自动回收(注意:全局变量是关闭页面后浏览器才会自动销毁,函数里面的变量,在函数调用完毕以后,自动销毁),所以垃圾就是指:不再使用的内存。 垃圾回收机制就是指:一个程序不再使用的内存(无用内存),Javascript 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。其会被js引擎‘自动回收’; 如果不能自动回收,就造成‘内存泄露’。
一、垃圾回收机制详解
1、什么是js的回收机制
垃圾回收机制的原理是找到不再继续使用的变量,释放其内存。垃圾回收器会按照固定的时间间隔(或代码中预定的收集时间),周期性地执行这一操作; Javascript 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。Javascript 中存在两种变量——全局变量和局部变量,全局变量的生命周期会一直持续,直到页面卸载;而局部变量声明在函数中,它的生命周期从执行函数开始,直到函数执行结束。在这个过程中,局部变量会在堆或栈上被分配相应的空间以存储它们的值,函数执行结束,这些局部变量也不再被使用,它们所占用的空间也就被释放;
实例如下:
function fn1() {
var obj = {name: 'qdleader', age: 10};
}
function fn2() {
var obj = {name:'qdleader', age: 10};
return obj;
}
var a = fn1();
var b = fn2();
fn1中定义的obj为局部变量,而当调用结束后,出了fn1的环境,那么该块内存会被js引擎中的垃圾回收器自动释放;在fn2被调用的过程中,返回的对象被全局变量b所指向,所以该块内存并不会被释放。
垃圾回收的两种实现方式:标记清除、引用计数 2、标记清除(主流浏览器做法) 标记-清除: 这是JavaScript主要采用的垃圾回收算法。它分为两个阶段:
标记阶段: 垃圾回收器会从根部开始遍历所有的对象,标记所有能够被访问到的对象。 清除阶段: 垃圾回收器会清除未被标记的对象,释放它们占用的内存
内部算法 垃圾回收的基本算法被称为 “mark-and-sweep”。
定期执行以下“垃圾回收”步骤:
垃圾收集器找到所有的根,并“标记”(记住)它们。 然后它遍历并“标记”来自它们的所有引用。 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。 ……如此操作,直到所有可达的(从根部)引用都被访问到。 没有被标记的对象都会被删除。
增量标记(Incremental Marking):
(为了减少垃圾回收对应用程序的影响,一些现代浏览器实现了增量标记。增量标记将标记阶段分成多个小步骤,每执行一小步骤后,允许应用程序继续执行一些代码。这有助于分散垃圾回收器的工作负担,减少阻塞时间。)
3、引用计数(IE9以下)
含义是跟踪记录所有值被引用的次数;
例如变量a赋值后,这个值的引用次数为1,这个a值又被赋值给另一个变量b,这时引用次数+1;但当b赋另外的值,引用次数-1;当值的引用书为0,说明没有办法再访问这个值,这时就可以将内存回收了。
IE9以下还在使用引用计数,当对象循环引用时,引用次数无法标记为0,就会导致无法被回收。其他浏览器废弃使用;
下面的代码
var o1 = {
o2: {
x: 1
}
};
// 两个对象被创建。
// ‘o1’对象引用‘o2’对象作为其属性。
// 不可以被垃圾收集
var o3 = o1; // ‘o3’变量是第二个引用‘o1‘指向的对象的变量.
o1 = 1; // 现在,在‘o1’中的对象只有一个引用,由‘o3’变量表示
var o4 = o3.o2; // 对象的‘o2’属性的引用.
// 此对象现在有两个引用:一个作为属性、另一个作为’o4‘变量
o3 = '374'; // 原来在“o1”中的对象现在为零,对它的引用可以垃圾收集。
// 但是,它的‘o2’属性存在,由‘o4’变量引用,因此不能被释放。
o4 = null; // ‘o1’中最初对象的‘o2’属性对它的引用为零。它可以被垃圾收集。
二、Js常见的内存泄漏
内存泄漏是应用程序过去使用,但不再需要的尚未返回到操作系统或可用内存池的内存片段。由于没有被释放而导致的,它将可能引起程序的卡顿和崩溃;
1、意外的全局变量
function foo(arg) {
bar = "test";
// window.bar = "test";
}
js对未声明变量会在全局最高对象上创建它的引用,(是以属性存在的,而不是变量),如果在游览器上就是window对象,如果在node环境下就是global;如果未声明的变量缓存大量的数据,它可能只有在页面被刷新或者被关闭的时候才会释放内存,这样就造成了内存意外泄漏;
2、被忘记的定时器或者回调函数
我们以经常在 JavaScript 中使用的 setInterval 为例;
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每5秒执行一次.
上面的代码片段显示了使用定时器引用节点或无用数据的后果。它既不会被收集,也不会被释放。无法被垃圾收集器收集,频繁的被调用,占用内存; 而正确的使用方法是,确保一旦依赖于它们的事件已经处理完成,就通过明确的调用来删除它们;
3、闭包 闭包是一个函数A返回一个内联的函数B,及时A函数执行完函数B也可以访问函数A里面的变量,这就是一个简单的闭包; 本质上闭包是将函数内部和外部连接起来的一座桥梁;
function my(name) {
function sendName() {
console.log(name)
}
return sendName
}
var test=my("test")
test() //test
在my()内部创建的sendName()函数是不会被回收的,因为它被全局变量test引用,处于随时被调用的状态。如果向释放内存可以设置test=null;由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多
4、DOM泄漏
浏览器中DOM和js采用的是不一样的引擎,DOM采用的是渲染引擎,而js采用的是v8引擎,所以在用js操作DOM时会比较耗费性能,因为他们需要桥来链接他们。为了减少DOM的操作,我们一般将常用的DOM; 我们会采用变量引用的方式会将其缓存在当前环境。如果在进行一些删除、更新操作之后,可能会忘记释放已经缓存的DOM;
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://test.png';
}
function removeImage() {
// image 元素是body的直接子元素。
document.body.removeChild(document.getElementById('image'));
// 我们仍然可以在全局元素对象中引用button。换句话说,button元素仍在内存中,无法由GC收集
}
一些优化建议:
分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。在典型的代码中,许多对象的生命周期都很短:它们出现、完成它们的工作并很快死去,因此在这种情况下跟踪新对象并将其从内存中清除是有意义的。那些长期存活的对象会变得“老旧”,并且被检查的频次也会降低。
增量收集(Incremental collection)—— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。因此,引擎将现有的整个对象集拆分为多个部分,然后将这些部分逐一清除。这样就会有很多小型的垃圾收集,而不是一个大型的。这需要它们之间有额外的标记来追踪变化,但是这样会带来许多微小的延迟而不是一个大的延迟。
闲时收集(Idle-time collection)—— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响