zhuzhuaicoding / earlyzhuzhu.github.io

Summary

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

如何使用chrome查内存泄漏

zhuzhuaicoding opened this issue · comments

缘由

vue一个issue,看到作者改动,只添加了一行代码(大神就是大神,仅仅一行代码,但是这行代码背后值得我去研究下),那么大神是怎么找到这个原因的呢?
我自己跟踪了这个issue里面的提供的reproduce link,从这个页面下手的。
想看内存是否泄漏了,可以从3个地方看:

  1. 任务管理器你看这个标签页下的内存占用,和js内存
  2. Timeline跟踪操作,在计数器窗口里查看js heap和Nodes的增长曲线,如果是成阶梯样,就有可能存在内存泄漏了(可以鼠标hover到对应的内存增长的陡点附近,定位这一时刻js都干了些什么)
  3. 使用Memory panel,查看内存快照

如何使用memory快照定位内存泄漏

Overview

直观感受:

  • 页面的性能随着时间的延长越来越差。 这可能是内存泄漏的症状。 内存泄漏是指,页面中的错误导致页面随着时间的延长使用的内存越来越多。
  • 页面的性能一直很糟糕。 这可能是内存膨胀的症状。 内存膨胀是指,页面为达到最佳速度而使用的内存比本应使用的内存多。
  • 页面出现延迟或者经常暂停。 这可能是频繁垃圾回收的症状。 垃圾回收是指浏览器收回内存。 浏览器决定何时进行垃圾回收。 回收期间,所有脚本执行都将暂停。因此,如果浏览器经常进行垃圾回收,脚本执行就会被频繁暂停。

任务管理器
Memory 原生内存,DOM节点存储在这里
JS Memory 列表示 JS 堆。此列包含两个值。 您感兴趣的值是实时数字(括号中的数字)。 实时数字表示您的页面上的可到达对象正在使用的内存量。 如果此数字在增大,要么是正在创建新对象,要么是现有对象正在增长。

Timeline
节点数是和代码对应的,如果内存慢慢增加,说明有泄漏。
Step:

  1. 打开record开关

  2. 按照issue里的复现步骤,点导航,点个几次

  3. 看计数器(主要看js heap和Nodes),可以看到曲线是在慢慢上升的(其实曲线是上升再下降的,成脉冲那种阶梯样,但是总的内存是在增加的,下降的原因是因为一些GC了)
    其实这个看也只是看个大概趋势,真正找到代码级别的内存泄漏的部分,还是得靠Memory Profile。

  4. 首先保存一个干净的内存快照

  5. 我点了3次不同的导航,在图里生成了3个detached dom(我用了Comparsion视图)

  6. 我随便点了其中一个,展开可以看到下面的retaining path
    4,可以看到下面的retainers窗口里有个pendingInsert这个对象
    2017-06-16 15-21-47

术语

  • Shallow size 对象自身的大小

  • Retained size 对象持有的引用,当被GC时,这一块大小会全部释放

  • Distance 到root的距离

  • Constructor 构造器 表示由这个构造器生成的同一类型的对象

  • Object count 对象的个数

  • 黄色背景的对象
    是js代码的直接引用,

  • 红色背景的对象

  • 未被js直接引用的,但是作为黄色节点的一部分而存活(本应该被GC的)
    黄色的节点,是存在内存泄漏的,需要看下为什么这个东西没有被GC

在下面的对象面板里查看具体哪个对象在引用上面的黄色节点

  • Object面板 查看Constructor面板中你选中的节点,在哪些地方被引用到了

  • 四种视图
    Summary --- 特别适合跟踪DOM泄漏
    Comparision
    Containment - 查看堆的内容,这里也可以看到detached dom tree,同样可以看到pendingInsert这个对象
    2017-06-16 21-07-01

Statistics
比较有用的就是Comparision了,比较2次快照,找到哪些对象本应该被GC的。

mark~

mark

看到 An interesting kind of JavaScript memory leak 这篇post
一时没搞懂这段代码为什么会导致内存泄露,这里记录一下分析结果

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);

这段代码的执行过程是:每隔1s执行replaceThing,可以看到originalThing每次执行都是保存上一次的theThing,theThing是global变量,这样会theThing通过someMethod的lexical scope,引用上一次的originalThing,这样形成一条链,可以从图3看出

通过Memory的比较视图查看内存泄露原因

3
longStr是 theThing引用的
2
由于unused引用了free variable originalThing 形成了一个闭包,导致someMethod的Lexical Environment也引用了originalThing(因为someMthond和unused是共用一个parent lexical scope的),引用关系就是longStr -> originalThing(@91637) -> someMethod's closure context -> someMethod -> window.theThing (-> 表示 reference by)
1
上面的引用关系是longStr -> originalThing -> someMethod's closure context -> someMethod -> originalThing(对比上面一张图,可以看到就是@91637对象) -> someMethod's closure context -> ...同上

概念

(compiled code): 编译后的js代码
(array): 内部array对象
Array: array对象
Object: js普通对象
(closure): 闭包 === function.
system / Context: function context . 也就是 这个function的lexical environment
system: 内部数据

Context Variable

capture_2018112_170952
closure表示一个函数。
context variable是保存在function context中的变量。function context含有闭包的lexical environment ,其中含有这个闭包执行需要的变量。

Context

每个function在创建时,都会创建一个lexical environment,它存在一个parent context的引用。闭包创建的时候,v8会生成一个Context对象,这个对象里有所有这个闭包需要的绑定。
上面的someMethod的context对应的就是这个上下文。
capture_2018112_170716

function makeF() {
  var x = 11;
  var y = 22;
  return function (what) {
    switch (what) {
      case "x": return x;
      case "y": return y;
    }
  }
}

var f = makeF();
f("x");

contexts-0

Reference