像C语言这样的高级语言一般都有底层的内存管理接口,比如 malloc()free()。另一方面,JavaScript创建变量(对象,字符串等)时分配内存,并且在不再使用它们时“自动”释放。 后一个过程称为垃圾回收(garbage collection ,简称GC)。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者感觉他们可以不关心内存管理,这是错误的。

内存生命周期

不管什么程序语言,内存生命周期基本是一致的:   

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放\归还

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

  JavaScript 的内存分配

  值的初始化

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

 1 var n = 123; // 给数值变量分配内存
 2 var s = "azerty"; // 给字符串分配内存
 3 
 4 var o = {
 5   a: 1,
 6   b: null
 7 }; // 给对象及其包含的值分配内存
 8 
 9 // 给数组及其包含的值分配内存(就像对象一样)
10 var a = [1, null, "abra"]; 
11 
12 function f(a){
13   return a + 2;
14 } // 给函数(可调用的对象)分配内存
15 
16 // 函数表达式也能分配一个对象
17 someElement.addEventListener('click', function(){
18   someElement.style.backgroundColor = 'blue';
19 }, false);

  通过函数调用分配内存

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

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

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

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

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一个新的字符串

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

  使用值

   使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数

  当内存不再需要使用时释放

  大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。

  高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。

垃圾回收

  如上文所述自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。接下来将解释必要的概念,了解主要的垃圾回收算法和它们的局限性。

  引用

  垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个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();

  实际例子

  IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:

var div;
window.onload = function(){
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("*");
};

  在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里,即使其从DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),这个数据占用的内存将永远不会被释放。

  标记-清除算法

  这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

  这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

  这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

  从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

  循环引用不再是问题了

  在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。

  限制: 那些无法从根对象查询到的对象都将被清除

  尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。

   GC的缺陷

  和其他语言一样,javascript的GC策略也无法避免一个问题:GC时,停止响应其他操作,这是为了安全考虑。而Javascript的GC在100ms甚至以上,对一般的应用还好,但对于JS游戏,动画对连贯性要求比较高的应用,就麻烦了。这就是新引擎需要优化的点:避免GC造成的长时间停止响应。

V8 详细信息

JavaScript 对象表示

存在三种原语类型:

  • 数字(例如 3.14159..)
  • 布尔值(true 或 false)
  • 字符串(例如“Werner Heisenberg”)

它们无法引用其他值,并且始终是叶或终止节点。

数字可以存储为:

  • 中间 31 位整型值(称为小整型 (SMI)),或
  • 堆对象,作为堆数字引用。堆数字用于存储不适合 SMI 格式的值(例如双精度),或者在需要将值“包装”起来时使用(例如在值上设置属性)。

字符串可以存储在以下位置:

  • VM 堆中,或
  • 渲染器内存中(外部)。将创建一个包装器对象并用于访问外部存储空间,例如,外部存储空间是存储脚本源和从网页接收(而不是复制到 VM 堆上)的其他内容的位置。

新 JavaScript 对象的内存分配自专用的 JavaScript 堆(或 VM 堆)。这些对象由 V8 的垃圾回收器管理,因此,只要存在一个对它们的强引用,它们就会一直保持活动状态。

原生对象是 JavaScript 堆之外的任何对象。与堆对象相反,原生对象在其生命周期内不由 V8 垃圾回收器管理,并且只能使用其 JavaScript 包装器对象从 JavaScript 访问。

Cons 字符串是一种由存储并联接的成对字符串组成的对象,是串联的结果。cons 字符串内容仅根据需要进行联接。一个示例便是需要构造已联接字符串的子字符串。

例如,如果您将 a 与 b 串联,您将获得一个字符串 (a, b),它表示串联结果。如果您稍后将 d 与该结果串联,您将得到另一个 cons 字符串 ((a, b), d)。

数组 - 数组是一个具有数字键的对象。它们在 V8 VM 中广泛使用,用于存储大量数据。用作字典的成套键值对采用数组形式。

典型的 JavaScript 对象可以是两个数组类型之一,用于存储:

  • 命名属性,以及
  • 数字元素

数字元素如果属性数量非常少,可以将其存储在 JavaScript 对象自身内部。

映射 - 一种用于说明对象种类及其布局的对象。例如,可以使用映射说明用于快速属性访问的隐式对象层次结构。

对象组

  每个原生对象组都由保持对彼此的相互引用的对象组成。例如,在 DOM 子树中,每个节点都有一个指向其父级的链接,并链接到下一个子级和下一个同级,形成一个互连图。请注意,原生对象不会在 JavaScript 堆中表示 - 这正是它们的大小为什么为零的原因。相反,创建包装器对象。

  每个包装器对象都会保持对相应原生对象的引用,用于将命令重定向到自身。这样,对象组会保持包装器对象。不过,这不会形成一个无法回收的循环,因为 GC 非常智能,可以释放包装器对象不再被引用的对象组。但是,忘记释放单个包装器将保持整个组和关联的包装器。

内存泄漏

  内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。在C++中,因为是手动管理内存,内存泄露是经常出现的事情。而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。

 1,闭包可以维持函数内局部变量,使其得不到释放。

function bindEvent() { 
    var obj=document.createElement("XXX"); 
    obj.onclick=function(){ 
        //Even if it's a empty function 
    } 
}

  

  上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调的引用外暴了,形成了闭包
  解决之道,将事件处理函数定义在外部,解除闭包
function bindEvent() { 
    var obj=document.createElement("XXX"); 
    obj.onclick=onclickHandler; 
} 
function onclickHandler(){ 
    //do something 
}

或者在定义事件处理函数的外部函数中,删除对dom的引用(题外,《JavaScript权威指南》中介绍过,闭包中,作用域中没用的属性可以删除,以减少内存消耗。)

function bindEvent() { 
    var obj=document.createElement("XXX"); 
    obj.onclick=function(){ 
        //Even if it's a empty function 
    } 
    obj=null; 
}

    2,删除属性而未删除引用

a = {p: {x: 1}};
b = a.p;
delete a.p;

执行这段代码之后b.x的值依然是1.由于已经删除的属性引用依然存在,因此在JavaScript的某些实现中,可能因为这种不严谨的代码而造成内存泄露。所以在销毁对象的时候,要遍历属性中属性,依次删除。又比如:

var select = document.querySelector;
var treeRef = select("#tree");
var leafRef = select("#leaf");
var body = select("body");

body.removeChild(treeRef);

//#tree can't be GC yet due to treeRef
treeRef = null;

//#tree can't be GC yet due to indirect 
//reference from leafRef

leafRef = null;
//#NOW can be #tree GC

只有页面的 DOM 树或 JavaScript 代码不再引用 DOM 节点时,DOM 节点才会被作为垃圾进行回收。

#leaf 可以维持对其父级 (parentNode) 的引用,并以递归方式返回 #tree,因此,只有 leafRef 被作废后,#tree 下的整个树才会成为 GC 的候选。

 3,Memory and V8 hidden classes【http://slides.com/gruizdevilla/memory#/5/13

      注:这个demo中的泄漏已经在新版chrome修复

 4,Timers

var buggyObject = {
 callAgain: function () {
  var ref = this;
  var val = setTimeout(function () {
   console.log('Called again: ' 
   + new Date().toTimeString()); 
   ref.callAgain();
  }, 1000);
 }
};

buggyObject.callAgain();
buggyObject = null;

5,Closureshttp://slides.com/gruizdevilla/memory#/5/18

闭包可能是其他内存泄漏的源头,要知道哪些引用还在闭包内保留,另外,eval is evil

6,window.open / target="_blank"

  在谷歌(仅测谷歌),当调用window.open 并且没有 noopener 配置的情况下打开当前域名下其他链接的话,那么opened将共享opener的内存,此时performance.memory中 jsHeapSizeLimit 属性的限制为 opened memory+ opener memory

  即当opend 和opener 的内存之和超过 这个属性值,页面即崩溃,故当使用window.open同时打开多个tab,页面很容易发生崩溃。这应该算是 内存膨胀 

 

  解决办法是 ,使用下边代码代替 window.open,关于 rel noopener 《https://mathiasbynens.github.io/rel-noopener/》《https://developers.google.com/web/tools/lighthouse/audits/noopener?hl=zh-cn》

function openNonpener (url) {
  let link = document.createElement('a')
  link.setAttribute('href', url)
  link.setAttribute('target', '_blank')
  link.setAttribute('rel', 'noopener')
  link.click()
}

 

 7. vue 相关

参考 《一个Vue页面的内存泄露分析

查看和调试

performance.memory

参考:

posted on 2018-08-13 15:21  无厘取笑  阅读(98)  评论(0)    收藏  举报