前言:内存泄漏写任何语言都必须得注意的问题,我司技术老大平常吐槽:之前作游戏的改内存泄漏的bug,如今写前端仍是这些问题。javascript
内存泄漏能够定义为程序再也不使用或不须要的一块内存,可是因为某种缘由没有被释放仍然被没必要要的占有。在代码中建立对象和变量会占用内存,可是javaScript是有本身的内存回收机制,能够肯定那些变量再也不须要,并将其清除。可是当你的代码存在逻辑缺陷的时候,你觉得你已经不须要,可是程序中还存在着引用,致使程序运行完后并无合适的回收所占用的空间,致使内存不断的占用,运行的时间越长占用的就越多,随之出现的是,性能不佳,高延迟,频繁崩溃。
在深刻了解内存泄漏以前,咱们须要知道一下几点:前端
无论什么程序语言,内存生命周期基本是一致的:文章封面图java
全部语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。node
不一样的语言经过不一样的方式来处理其内存。算法
垃圾回收机制经过按期的检查哪些先前分配的内存“仍然被须要”(×)
垃圾回收机制经过按期的检查哪些先前分配的内存“程序的其余部分仍然能够访问到的内存”(√)。数组
嗯? 一脸懵逼。。。。浏览器
这是理解垃圾回收的关键,只有开发者知道这一块内存是否在将来使用“被须要”,可是能够经过算法来肯定没法访问的内存并将其标记返回操做系统。session
这是最初级的垃圾收集算法,若是没有引用指向该对象(零引用),对象将被垃圾回收机制回收。(MDN上面的例子)闭包
var o = { a: { b:2 } }; // 两个对象被建立,一个做为另外一个的属性被引用, 另外一个被分配给变量o // 很显然,没有一个能够被垃圾收集 var o2 = o; // o2变量是第二个对“这个对象”的引用 o = 1; // 如今,“这个对象”的原始引用o被o2替换了 var oa = o2.a; // 引用“这个对象”的a属性 // 如今,“这个对象”有两个引用了,一个是o2,一个是oa o2 = "yo"; // 最初的对象如今已是零引用了 // 他能够被垃圾回收了 // 然而它的属性a的对象还在被oa引用,因此还不能回收 oa = null; // a属性的那个对象如今也是零引用了 // 它能够被垃圾回收了
缺陷:在循环的状况下,引用计数算法存在很大的局限性。并发
function foo(){ var obj1 = {}; var obj2 = {}; obj1.x = obj2 ; // obj1引用obj2 obj2.x = obj1 ; // obj2引用obj1 return true ; } foo();
算法由如下几步组成:
无图言( )
循环引用的问题迎刃而解.虽然这个算法在不停的改进,js垃圾回收的(生成/增量/并发垃圾回收)这些改进的本质上仍是相同的: 可达内存被标记,其他的被看成垃圾回收。
缺点: 算法运行时程序执行被暂停。
### 垃圾回收机制不可预测
虽然垃圾回收机制很好很方便,可是得本身权衡.缘由是咱们没法肯定什么时候会执行收集.只有开发人员才能明确是否能够将一块内存返回给操做系统。不须要的引用是指开发者明知内存引用再也不须要,而咱们在写程序的时候却因为某些缘由,它仍被留在激活的 root 树中。
### 如何避免
垃圾收集语言泄漏的主要缘由是不须要的引用。要了解不须要的引用是什么,首先咱们须要了解垃圾收集器如何肯定是否能够访问一块内存。
所以,要了解哪些是JavaScript中最多见的泄漏,咱们须要知道引用一般被遗忘的方式。
### 常见的四种内存泄漏
#### 1. 全局变量
在非严格模式下当引用未声明的变量时,会在全局对象中建立一个新变量。在浏览器中,全局对象将是window,这意味着
function foo(arg){ bar =“some text”; // bar将泄漏到全局. }
为何不能泄漏到全局呢,咱们平时都会定义全局变量呢!!!
缘由 :全局变量是根据定义没法被垃圾回收机制收集.须要特别注意用于临时存储和处理大量信息的全局变量。若是必须使用全局变量来存储数据,请确保将其指定为null或在完成后从新分配它。
解决办法 : 严格模式
var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { node.innerHTML = JSON.stringify(someResource)); // 定时器也没有清除 } // node、someResource 存储了大量数据 没法回收 }, 1000);
缘由:与节点或数据关联的计时器再也不须要,node 对象能够删除,整个回调函数也不须要了。但是,计时器回调函数仍然没被回收(计时器中止才会被回收)。同时,someResource 若是存储了大量的数据,也是没法被回收的。
解决方法: 在定时器完成工做的时候,手动清除定时器
var refA = document.getElementById('refA'); document.body.removeChild(refA); // dom删除了 console.log(refA, "refA"); // 可是还存在引用 能console出整个div 没有被回收
缘由: 保留了DOM节点的引用,致使GC没有回收
解决办法:refA = null;
注意: 此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td> 的引用。未来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 之外的其它节点。实际状况并不是如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。因为代码保留了 <td> 的引用,致使整个表格仍待在内存中。保存 DOM 元素引用的时候,要当心谨慎。
注意
注意
注意: 闭包自己没有错,不会引发内存泄漏.而是使用错误致使.
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000);
这是一段糟糕的代码,每次调用 replaceThing ,theThing 获得一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了theThing)。思绪混乱了吗?最重要的事情是,闭包的做用域一旦建立,它们有一样的父级做用域,做用域是共享的。someMethod 能够经过 theThing 使用,someMethod 与 unused 分享闭包做用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并没有法下降内存占用。本质上,闭包的链表已经建立,每个闭包做用域携带一个指向大数组的间接的引用,形成严重的内存泄漏。
解决: 去除unuserd函数或者在replaceThing函数最后一行加上 originlThing = null.
参考:
4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
How JavaScript works: memory management + how to handle 4 common memory leaks