陪你秋招系列: 完全掌握js内存泄漏以及如何避免


前言:内存泄漏写任何语言都必须得注意的问题,我司技术老大平常吐槽:之前作游戏的改内存泄漏的bug,如今写前端仍是这些问题。javascript

什么是内存泄漏

内存泄漏能够定义为程序再也不使用或不须要的一块内存,可是因为某种缘由没有被释放仍然被没必要要的占有。在代码中建立对象和变量会占用内存,可是javaScript是有本身的内存回收机制,能够肯定那些变量再也不须要,并将其清除。可是当你的代码存在逻辑缺陷的时候,你觉得你已经不须要,可是程序中还存在着引用,致使程序运行完后并无合适的回收所占用的空间,致使内存不断的占用,运行的时间越长占用的就越多,随之出现的是,性能不佳,高延迟,频繁崩溃。
在深刻了解内存泄漏以前,咱们须要知道一下几点:前端

  1. 内存生命周期
  2. 内存管理系统
  3. 垃圾回收算法

内存生命周期

无论什么程序语言,内存生命周期基本是一致的:文章封面图java

  • 分配你所须要的内存
  • 使用分配到的内存(读、写)
  • 不须要时将其释放归还

全部语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。node

内存管理系统:手动or自动

不一样的语言经过不一样的方式来处理其内存。算法

  • 低级语言:像C语言这样的低级语言通常都有底层的内存管理接口,好比 malloc()和free()。
  • 高级语言:JavaScript是在建立变量(对象,字符串等)时自动进行了分配内存,而且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其余高级语言)开发者错误的感受他们能够不关心内存管理。

垃圾回收算法

垃圾回收机制经过按期的检查哪些先前分配的内存“仍然被须要”(×)
垃圾回收机制经过按期的检查哪些先前分配的内存“程序的其余部分仍然能够访问到的内存”(√)。数组

嗯? 一脸懵逼。。。。浏览器

这是理解垃圾回收的关键,只有开发者知道这一块内存是否在将来使用“被须要”,可是能够经过算法来肯定没法访问的内存并将其标记返回操做系统。session

  • 引用计数
  • 标记清除

1. 引用计数

这是最初级的垃圾收集算法,若是没有引用指向该对象(零引用),对象将被垃圾回收机制回收。(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();

2. 标记清除

算法由如下几步组成:

  1. 垃圾回收器建立了一个“roots”列表。Roots一般是代码中全局变量的引用。JavaScript中,“window”对象是一个全局变量,被看成root。window对象老是存在,所以垃圾回收器能够检查它和它的全部子对象是否存在(即不是垃圾);
  2. 全部的 roots被检查和标记为激活(即不是垃圾)。全部的子对象也被递归地检查。从root开始的全部对象若是是可达的,它就不被看成垃圾。
  3. 全部未被标记的内存会被当作垃圾,收集器如今能够释放内存,归还给操做系统了。

无图言( )

循环引用的问题迎刃而解.虽然这个算法在不停的改进,js垃圾回收的(生成/增量/并发垃圾回收)这些改进的本质上仍是相同的: 可达内存被标记,其他的被看成垃圾回收。

缺点: 算法运行时程序执行被暂停。

### 垃圾回收机制不可预测

虽然垃圾回收机制很好很方便,可是得本身权衡.缘由是咱们没法肯定什么时候会执行收集.只有开发人员才能明确是否能够将一块内存返回给操做系统。不须要的引用是指开发者明知内存引用再也不须要,而咱们在写程序的时候却因为某些缘由,它仍被留在激活的 root 树中。

### 如何避免

垃圾收集语言泄漏的主要缘由是不须要的引用。要了解不须要的引用是什么,首先咱们须要了解垃圾收集器如何肯定是否能够访问一块内存。

所以,要了解哪些是JavaScript中最多见的泄漏,咱们须要知道引用一般被遗忘的方式

### 常见的四种内存泄漏
#### 1. 全局变量

在非严格模式下当引用未声明的变量时,会在全局对象中建立一个新变量。在浏览器中,全局对象将是window,这意味着

function foo(arg){ 
    bar =“some text”; // bar将泄漏到全局.
}

为何不能泄漏到全局呢,咱们平时都会定义全局变量呢!!!

缘由 :全局变量是根据定义没法被垃圾回收机制收集.须要特别注意用于临时存储和处理大量信息的全局变量。若是必须使用全局变量来存储数据,请确保将其指定为null或在完成后从新分配它。
解决办法 : 严格模式

2. 被遗忘的定时器和回调函数

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
        // 定时器也没有清除
    }
    // node、someResource 存储了大量数据 没法回收
}, 1000);

缘由:与节点或数据关联的计时器再也不须要,node 对象能够删除,整个回调函数也不须要了。但是,计时器回调函数仍然没被回收(计时器中止才会被回收)。同时,someResource 若是存储了大量的数据,也是没法被回收的。

解决方法: 在定时器完成工做的时候,手动清除定时器

3. DOM引用

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 元素引用的时候,要当心谨慎。

4. 闭包

注意

注意

注意: 闭包自己没有错,不会引发内存泄漏.而是使用错误致使.

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

相关文章
相关标签/搜索