Chrome 浏览器垃圾回收机制与内存泄漏分析

垃圾回收机制

本部分的内容引用自《浏览器工做原理与实践》html

一般状况下,垃圾数据回收分为手动回收自动回收两种策略。前端

手动回收策略,什么时候分配内存、什么时候销毁内存都是由代码控制的。node

自动回收策略,产生的垃圾数据是由垃圾回收器来释放的,并不须要手动经过代码来释放。git

JavaScript 中调用栈中的数据回收

JavaScript 引擎会经过向下移动 ESP(记录当前执行状态的指针) 来销毁该函数保存在栈中的执行上下文。github

JavaScript 堆中的数据回收

在 V8 中会把堆分为新生代老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。web

新生区一般只支持 1~8M 的容量,而老生区支持的容量就大不少了。对于这两块区域,V8 分别使用两个不一样的垃圾回收器,以便更高效地实施垃圾回收。算法

  • 副垃圾回收器,主要负责新生代的垃圾回收。
  • 主垃圾回收器,主要负责老生代的垃圾回收。

不论什么类型的垃圾回收器,它们都有一套共同的执行流程。chrome

  1. 第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是能够进行垃圾回收的对象。
  2. 第二步是回收非活动对象所占据的内存。其实就是在全部的标记完成以后,统一清理内存中全部被标记为可回收的对象。
  3. 第三步是作内存整理。通常来讲,频繁回收对象后,内存中就会存在大量不连续空间,咱们把这些不连续的内存空间称为内存碎片,。当内存中出现了大量的内存碎片以后,若是须要分配较大连续内存的时候,就有可能出现内存不足的状况。因此最后一步须要整理这些内存碎片。(这步实际上是可选的,由于有的垃圾回收器不会产生内存碎片).
新生代中垃圾回收

新生代中用Scavenge 算法来处理,把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。新加入的对象都会存放到对象区域,当对象区域快被写满时,就须要执行一次垃圾清理操做。数组

在垃圾回收过程当中,首先要对对象区域中的垃圾作标记;标记完成以后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,因此这个复制过程,也就至关于完成了内存整理操做,复制后空闲区域就没有内存碎片了。浏览器

完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操做,同时这种角色翻转的操做还能让新生代中的这两块区域无限重复使用下去.

为了执行效率,通常新生区的空间会被设置得比较小,也正是由于新生区的空间不大,因此很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是通过两次垃圾回收依然还存活的对象,会被移动到老生区中。

老生代中的垃圾回收

老生代中用标记 - 清除(Mark-Sweep)的算法来处理。首先是标记过程阶段,标记阶段就是从一组根元素开始,递归遍历这组根元素(遍历调用栈),在这个遍历过程当中,能到达的元素称为活动对象,没有到达的元素就能够判断为垃圾数据.而后在遍历过程当中标记,标记完成后就进行清除过程。它和副垃圾回收器的垃圾清除过程彻底不一样,这个的清除过程是删除标记数据。

清除算法后,会产生大量不连续的内存碎片。而碎片过多会致使大对象没法分配到足够的连续内存,因而又产生了标记 - 整理(Mark-Compact)算法,这个标记过程仍然与标记 - 清除算法里的是同样的,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存,从而让存活对象占用连续的内存块。

全停顿

因为 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都须要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。咱们把这种行为叫作全停顿

在 V8 新生代的垃圾回收中,因其空间较小,且存活对象较少,因此全停顿的影响不大,但老生代就不同了。若是执行垃圾回收的过程当中,占用主线程时间太久,主线程是不能作其余事情的。好比页面正在执行一个 JavaScript 动画,由于垃圾回收器在工做,就会致使这个动画在垃圾回收过程当中没法执行,这将会形成页面的卡顿现象。

为了下降老生代的垃圾回收而形成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,咱们把这个算法称为增量标记(Incremental Marking)算法.

使用增量标记算法,能够把一个完整的垃圾回收任务拆分为不少小的任务,这些小的任务执行时间比较短,能够穿插在其余的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户由于垃圾回收任务而感觉到页面的卡顿了。

内存泄漏

再也不用到的内存,没有及时释放,就叫作内存泄漏(memory leak)。

内存泄漏发生的缘由

  1. 缓存

有时候为了方便数据的快捷复用,咱们会使用缓存,可是缓存必须有一个大小上限才有用。高内存消耗将会致使缓存突破上限,由于缓存内容没法被回收。

  1. 队列消费不及时

当浏览器队列消费不及时时,会致使一些做用域变量得不到及时的释放,于是致使内存泄漏。

  1. 全局变量

除了常规设置了比较大的对象在全局变量中,还多是意外致使的全局变量,如:

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

在函数中,没有使用 var/let/const 定义变量,这样其实是定义在window上面,变成了window.bar。 再好比因为this致使的全局变量:

function foo() {
    this.bar = "this is a hidden global variable";
}
foo()
复制代码

这种函数,在window做用域下被调用时,函数里面的this指向了window,执行时实际上为window.bar=xxx,这样也产生了全局变量。

  1. 计时器中引用没有清除

先看以下代码:

var someData = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someData));
    }
}, 1000);
复制代码

这里定义了一个计时器,每隔1s把一些数据写到Node节点里面。可是当这个Node节点被删除后,这里的逻辑其实都不须要了,但是这样写,却致使了计时器里面的回调函数没法被回收,同时,someData里的数据也是没法被回收的。

  1. 闭包

看如下这个闭包:

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);
复制代码

每次调用 replaceThingtheThing 会建立一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing(theThing) 的闭包,闭包的做用域一旦建立,它们有一样的父级做用域,做用域是共享的。

someMethod 能够经过 theThing 使用,someMethodunused 分享闭包做用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。

所以,当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并没有法下降内存占用。

本质上,闭包的链表已经建立,每个闭包做用域携带一个指向大数组的间接的引用,形成严重的内存泄漏。

  1. 事件监听

例如,Node.js 中 Agent 的 keepAlive 为 true 时,可能形成的内存泄漏。当 Agent keepAlive 为 true 的时候,将会复用以前使用过的 socket,若是在 socket 上添加事件监听,忘记清除的话,由于 socket 的复用,将致使事件重复监遵从而产生内存泄漏。

内存泄漏的识别方法

  1. 使用 Chrome 任务管理器实时监视内存使用 打开 chrome 浏览器,点击右上角主菜单,选择更多工具->任务管理器,这样就开启了任务管理器面板,而后再右键点击任务管理器的表格标题并启用 JavaScript使用的内存,能看到这样的面板:

下面两列能够告诉您与页面的内存使用有关的不一样信息:

GitHub

  1. 内存占用空间(Memory) 列表示原生内存。DOM 节点存储在原生内存中。 若是此值正在增大,则说明正在建立 DOM 节点。
  2. JavaScript使用的内存(JavaScript Memory) 列表示 JS 堆。此列包含两个值。 您感兴趣的值是实时数字(括号中的数字)。实时数字表示您的页面上的可到达对象正在使用的内存量。 若是此数字在增大,要么是正在建立新对象,要么是现有对象正在增加。

当你页面稳定下来以后,这两个的值还在上涨,你就能够查一查是否内存泄漏了。

  1. 利用chrome 时间轴记录可视化内存泄漏

Performance(时间轴)可以面板直观实时显示JS内存使用状况、节点数量、监听器数量等。

打开 chrome 浏览器,调出调试面板(DevTools),点击Performance选项(低版本是Timeline),勾选Memory复选框。一种比较好的作法是使用强制垃圾回收开始和结束记录。在记录时点击 Collect garbage 按钮 (强制垃圾回收按钮) 能够强制进行垃圾回收。 因此录制顺序能够这样:开始录制前先点击垃圾回收-->点击开始录制-->点击垃圾回收-->点击结束录制。 面板介绍如图:

GitHub
录制结果如图:
GitHub
首先,从图中咱们能够看出不一样颜色的曲线表明的含义,这里主要关注JS堆内存、节点数量、监听器数量。鼠标移到曲线上,能够在左下角显示具体数据。在实际使用过程当中,若是您看到这种 JS 堆大小或节点大小不断增大的模式,则可能存在内存泄漏。

  1. 使用堆快照发现已分离 DOM 树的内存泄漏

只有页面的 DOM 树或 JavaScript 代码再也不引用 DOM 节点时,DOM 节点才会被做为垃圾进行回收。 若是某个节点已从 DOM 树移除,但某些 JavaScript 仍然引用它,咱们称此节点为“已分离”,已分离的 DOM 节点是内存泄漏的常见缘由。

同理,调出调试面板,点击Memory,而后选择Heap Snapshot,而后点击进行录制。录制完成后,选中录制结果,在 Class filter 文本框中键入 Detached,搜索已分离的 DOM 树。 以这段代码为例:

<html>
<head>
</head>
<body>
<button id="createBtn">增长节点</button>
<script> 
var detachedNodes;

function create() {
  var ul = document.createElement('ul');
  for (var i = 0; i < 10; i++) {
    var li = document.createElement('li');
    ul.appendChild(li);
  }
  detachedTree = ul;
}

document.getElementById('createBtn').addEventListener('click', create);
</script>
</body>
</html>
复制代码

点击几下,而后记录。能够获得如下信息:

GitHub
旧版的面板,还会有颜色标注,黄色的对象实例表示它被JS代码引用,红色的对象实例表示被黄色节点引用的游离节点。上图是新版本的,不会有颜色标识。可是仍是能够一个个来看,如上图,点开节点,能够看到下面的引用信息,上面能够看出,有个HTMLUListElement(ul节点)被window.detachedNodes引用。再结合代码,原来是没有加var/let/const声明,致使其成了全局变量,因此DOM没法释放。

  1. 按函数调查内存分配 打开面板,点击JavaScript Profiler,若是没看到这个选项,你能够点调试面板右上角的三个点,选择more tools,而后选择。

ps: chrome 旧版的浏览器,这个功能在 Profiles 里面,点Record Allocation Profile便可.

操做步骤:点start->在页面进行你要检测的操做->点stop。

GitHub
DevTools 按函数显示内存分配明细。默认视图为 Heavy (Bottom Up),将分配了最多内存的函数显示在最上方,还有函数的位置,你能够看看是哪些函数占用内存较多。

避免内存泄漏的方法

  1. 少用全局变量,避免意外产生全局变量
  2. 使用闭包要及时注意,有Dom元素的引用要及时清理。
  3. 计时器里的回调没用的时候要记得销毁。
  4. 为了不疏忽致使的遗忘,咱们可使用 WeakSetWeakMap结构,它们对于值的引用都是不计入垃圾回收机制的,表示这是弱引用。 举个例子:
const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"
复制代码

这种状况下,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

基本上,若是你要往对象上添加数据,又不想干扰垃圾回收机制,就可使用 WeakMap。

参考资料

最后

  • 欢迎加我微信(winty230),拉你进技术群,长期交流学习...
  • 欢迎关注「前端Q」,认真学前端,作个有专业的技术人...

GitHub
相关文章
相关标签/搜索