前端JS内存管理

前言

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

内存的生命周期

生命周期概念

不管是是使用什么编程语言,内存生命周期几乎都是同样的:程序员

生命周期的概述:算法

  • 内存分配(Allocate memory ):当咱们申明变量、函数、对象的时候,系统会自动为他们分配内存
  • 内存使用(Use memory ):即读写内存,也就是使用变量、函数等
  • 内存释放(Release memory ):使用完毕,由垃圾回收机制自动回收再也不使用的内存

内存的概念

在硬件层面,计算机内存是由大量的触发器)组成的。每个触发器都包含有一些晶体管,可以存储1比特。单个触发器可经过一个惟一标识符来寻址,这样咱们就能够读和写了。所以从概念上讲,咱们能够把计算机内存看做是一个巨大的比特数组,咱们能够对它进行读和写。编程

可是做为人类,咱们并不善于用比特来思考和运算,所以咱们将其组成更大些的分组,这样咱们就能够用来表示数字。8个比特就是一个字节。比字节大的有字(16比特或32比特)。数组

有不少东西都存储在内存中:浏览器

  1. 全部被程序使用的变量和其余数据
  2. 程序的代码,包括操做系统自身的代码

当你编译你的代码时,编译器能够检查原始的数据类型而且提早计算出将会须要多少内存。而后把所需的(内存)容量分配给调用栈空间中的程序。这些变量由于函数被调用而分配到的空间被称为堆栈空间,它们的内存增长在现存的内存上面(累加)。如它们再也不被须要就会按照 LIFO(后进,先出)的顺序被移除。例如,参见以下声明:缓存

int n; // 4 bytes
int x[4]; // array of 4 elements, each 4 bytes
double m; // 8 bytes

编译器能够当即清楚这段代码须要4 + 4 × 4 + 8 = 28字节。数据结构

这就是它怎样工做于当前的 integers 和 doubles 型的大小。约20年前,integers一般(占用)2字节,double占4字节。你的代码不该该依赖于此时刻的基本数据类型的大小。

编译器将插入些会互相做用于操做系统在堆栈上去请求必要的字节数来存储变量代码。闭包

在以上例子中,编译器知道每一个变量精确的内存地址。事实上,不管咱们什么时候写入变量n,而本质上这会被翻译为如“内存地址 4127963 ”。dom

JS内存分配

为了避免让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。

//给数值分配内存空间
var num = 1; 

//给字符串分配内存
var str = "hehe";

//给对象及其包含的值分配内存
var obj = {
  a: 1,
  b: "str",
  c: null
}

// 给数组及其包含的值分配内存(就像对象同样)
var a = [1, null, "abra"]; 

// 给函数(可调用的对象)分配内存
function f(a){
  return a + 2;
} 

// 函数表达式也能分配一个对象
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

有些函数调用结果是分配对象内存 以下:

var d = new Date(); // 分配一个 Date 对象

var e = document.createElement('div'); // 分配一个 DOM 元素

有些方法是分配新变量或者新对象 以下:

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一个新的字符串
// 由于字符串是不变量,
// JavaScript 可能决定不分配内存,
// 只是存储了 [0-3] 的范围。

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); 
// 新数组有四个元素,是 a 链接 a2 的结果

JS使用内存

基本上在 JavaScript 中使用分配的内存,就是对它进行读和写操做。

能够读写变量的值或某个对象的属性,甚至是给某个函数传递一个参数。

var a = 10; // 分配内存
console.log(a); // 对内存的使用

JS内存回收

当内存再也不须要的时候要释放掉

大部分的内存管理问题出如今这个阶段。

这里面最难的任务是指出,在何时分配的内存再也不被须要。这一般须要开发者来决定程序中的那一块内存再也不须要了,并释放。

高级语言嵌入了一个叫垃圾收集器的程序,它能够跟踪内存分配和使用状况,以找出在哪一种状况下某一块已分配的内存再也不被须要,并自动的释放它。

不幸的是,这种程序只是一种近似的操做,由于知道某块内存是否被须要是不可断定的)(并不能经过算法来解决)。

大部分的垃圾收集器的工做方式是收集那些不可以被再次访问的内存,好比超出做用域的变量。可是,可以被收集的内存空间是低于近似值的,由于在任什么时候候均可能存在一个在做用域内的变量指向一块内存区域,可是它永远不可以被再次访问。

再也不须要使用的变量也就是生命周期结束的变量,是局部变量,局部变量只在函数的执行过程当中存在, 当函数运行结束,没有其余引用(闭包),那么该变量会被标记回收。
全局变量的生命周期直至浏览器卸载页面才会结束,也就是说全局变量不会被当成垃圾回收。

由于自动垃圾回收机制的存在,开发人员能够不关心也不注意内存释放的有关问题,但对无用内存的释放这件事是客观存在的。 不幸的是,即便不考虑垃圾回收对性能的影响,目前最新的垃圾回收算法,也没法智能回收全部的极端状况。

垃圾回收

引用

垃圾回收算法主要依赖于引用的概念。

在内存管理的环境中,一个对象若是有访问另外一个对象的权限(隐式或者显式),叫作一个对象引用另外一个对象。

例如,一个Javascript对象具备对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

在这里,“对象”的概念不只特指 JavaScript 对象,还包括函数做用域(或者全局词法做用域)。

引用计数垃圾收集

这是最初级的垃圾回收算法。

引用计数算法定义“内存再也不使用”的标准很简单,就是看一个对象是否有指向它的引用。 若是没有其余对象指向它了,说明该对象已经再也不需了。

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 f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o  这里

  return "azerty";
}

f();

上面咱们申明了一个函数 f ,其中包含两个相互引用的对象。 在调用函数结束后,对象 o1 和 o2 实际上已离开函数范围,所以再也不须要了。 但根据引用计数的原则,他们之间的相互引用依然存在,所以这部份内存不会被回收,内存泄露不可避免了。

再来看一个实际的例子:

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};

上面这种JS写法再普通不过了,建立一个DOM元素并绑定一个点击事件。 此时变量 div 有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。 一个循序引用出现了,按上面所讲的算法,该部份内存无可避免的泄露了。

为了解决循环引用形成的问题,现代浏览器经过使用标记清除算法来实现垃圾回收。

标记清除算法

标记清除算法将“再也不使用的对象”定义为“没法达到的对象”。 简单来讲,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还须要使用的。 那些没法由根部出发触及到的对象被标记为再也不使用,稍后进行回收。

从这个概念能够看出,没法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是没法触及的对象)。 但反之未必成立。

工做流程:

  1. 垃圾收集器会在运行的时候会给存储在内存中的全部变量都加上标记。
  2. 从根部出发将能触及到的对象的标记清除。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工做,销毁那些带标记的值并回收它们所占用的内存空间。

img

循环引用再也不是问题了

再看以前循环引用的例子:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

函数调用返回以后,两个循环引用的对象在垃圾收集时从全局对象出发没法再获取他们的引用。 所以,他们将会被垃圾回收器回收。

内存泄露

概念

程序的运行须要内存。只要程序提出要求,操做系统或者运行时(runtime)就必须供给内存。

对于持续运行的服务进程(daemon),必须及时释放再也不用到的内存。 不然,内存占用愈来愈高,轻则影响系统性能,重则致使进程崩溃。

本质上讲,内存泄漏就是因为疏忽或错误形成程序未能释放那些已经再也不使用的内存,形成内存的浪费。

常见的JS内存泄露

1.意外的全局变量

JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象建立一个新变量。在浏览器中,全局对象是 window 。

function foo(arg) { 
    bar = "this is a hidden global variable"; 
}

事实上变量bar被解释成下面的状况:

function foo(arg) { 
    window.bar = "this is a hidden global variable"; 
}

函数 foo 内部忘记使用 var ,意外建立了一个全局变量。此例泄露了一个简单的字符串,无伤大雅,可是有更糟的状况。

由this建立的意外的全局变量:

function foo() { 
    this.variable = "potential accidental global"; 
} 
 
// Foo 调用本身,this 指向了全局对象(window) 
// 而不是 undefined 
foo()

在 JavaScript 文件头部加上 'use strict',能够避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。

全局变量使用注意事项

  1. 尽管咱们讨论了一些意外的全局变量,可是仍有一些明确的全局变量产生的垃圾。它们被定义为不可回收(除非定义为空或从新分配)。
  2. 全局变量用于 临时存储和处理大量信息时,须要多加当心。若是必须使用全局变量存储大量数据时,确保用完之后把它设置为 null 或者从新定义
  3. 与全局变量相关的增长内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。
  4. 高内存消耗致使缓存突破上限,由于缓存内容没法被回收。

2.没有释放的计时器或者回调函数

在 JavaScript 中使用 setInterval 很是日常。一段常见的代码:

var someResource = getData(); 
setInterval(function() { 
    var node = document.getElementById('Node'); 
    if(node) { 
        // 处理 node 和 someResource 
        node.innerHTML = JSON.stringify(someResource)); 
    } 
}, 1000);

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

对于观察者的例子,一旦它们再也不须要(或者关联的对象变成不可达),明确地移除它们很是重要。老的 IE 6 是没法处理循环引用的。现在,即便没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是能够回收观察者处理函数的。

var element = document.getElementById('button'); 
function onClick(event) { 
    element.innerHTML = 'text'; 
} 
 
element.addEventListener('click', onClick);

对象观察者和循环引用注意事项

老版本的 IE 是没法检测 DOM 节点与 JavaScript 代码之间的循环引用,会致使内存泄露。现在,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经能够正确检测和处理循环引用了。换言之,回收节点内存时,没必要非要调用 removeEventListener 了。

3.脱离DOM的引用

有时,保存 DOM 节点内部数据结构颇有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组颇有意义。此时,一样的 DOM 元素存在两个引用:一个在 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 回收。

此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td> 的引用。未来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 之外的其它节点。实际状况并不是如此:此<td> 是表格的子节点,子元素与父元素是引用关系。因为代码保留了 <td> 的引用,致使整个表格仍待在内存中。保存 DOM 元素引用的时候,要当心谨慎。

4.闭包

闭包是 JavaScript 开发的一个关键方面:匿名函数能够访问父级做用域的变量。

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)并没有法下降内存占用。本质上,闭包的链表已经建立,每个闭包做用域携带一个指向大数组的间接的引用,形成严重的内存泄露。

避免内存泄露

记住一个原则:不用的东西,及时归还。

  1. 减小没必要要的全局变量,使用严格模式避免意外建立全局变量。
  2. 在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
  3. 组织好你的逻辑,避免死循环等形成浏览器卡顿,崩溃的问题。
相关文章
相关标签/搜索