- 0133技术站
- 联系QQ:18840023
- QQ交流群

- 微信公众号

学习如何用Chrome DevTools 堆分析器录制堆快照并查找内存泄漏。
Chrome DevTools 堆分析器显示页面 JavaScript 对象和相关 DOM 节点的内存分配 (请参见Objects retaining tree)。使用它来获取 JS 堆快照、 分析内存图、 比较快照,查找内存泄漏。
在Profiles
(分析)面板中,选择 Take Heap Snapshot
(生成堆快照),然后点击 Start
(开始) 或者使用Cmd+E或Ctrl+E快捷键。
Snapshots
(快照)最初存储在渲染内存中。当您根据需要点击快照图标来查看它的时候,他们会转移到DevTools中。
在快照加载到DevTools并且已经解析之后, 快照标题下方显示一个数字,该数字表示所有可访问的 JavaScript 对象的总大小:
点击Clear all profiles
(清除所有分析)图标可以清除快照(包括 DevTools 中和渲染内存中都会删除掉):
直接关闭 DevTools 窗口并不会删除渲染内存中的分析文件。当重新打开 DevTools 窗口的时候,所有之前生成的快照都会在快照列表中出现。
不同的任务中使用不同角度查看快照:
Summary view
(摘要视图) 按构造函数名称分组显示对象。用它来基于类型分组追捕对象 (和内存使用) 。它对追踪 DOM 泄漏特别有帮助。
Comparison view
(比较视图) 显示两个快照之间的差异。用它来比较两个 (或更多) 内存快照的操作之前和之后。检查被释放的被引用的对象让您确认内存泄漏的原因。
Containment view
(包含视图) 允许堆内容的探索。它提供更好的对象结构的视图,帮助分析全局命名空间 (window)周围是什么对象在引用。用它来分析闭包或对象的更深层次对象。
Dominators view
(支配者视图) 显示[支配者树],并可用于发现积累(accumulation?)点。此视图可以帮助确认对象在删除/垃圾回收之前是否有外部引用任在工作。
视图之间进行切换,请使用视图底部的下拉框:
最开始的时候,快照是在总结视图中打开的,显示了对象的整体情况,并且该视图可以展开以显示实例信息:
顶级入口是 “total” 行,他们展示了:
Constructor
(构造器),表示所有用这个构造器创建的对象。Shallow Size
(浅尺寸) 这一列显示了当前构造器创建的所有对象的 Shallow Size
(浅尺寸) 总和。Shallow Size
(浅尺寸)是对象本身的内存的大小(一般,数组和字符串有较大的Shallow Size
(浅尺寸))。另请参见Object sizesRetained size
(保留尺寸) 这一列显示相同的对象集所对应的最大 Retained size
(保留尺寸)。删除对象 (依赖对象不再可达时) 可以释放的内存的大小被称为Retained size
(保留尺寸)。另请参见Object sizesDistance
(距离) 显示了从根节点开始,从节点的最短路径到达当前节点的距离。像上图那样展开 total line 之后,其所有的实例都会显示出来。对于每个实例,它的 shallow size 和 retained size 都会在相应列中展示出来。在 @
字符后面的数字就是对象的 ID,该 ID 允许你在每个对象的基础上比较堆的快照。
请记住,黄色的对象表示有 JavaScript 对象引用了它们,而红色的对象是指从一个黄色背景节点引用的分离节点(detached nodes)。
在堆分析器中不同的Constructor
(构造器)对应什么功能?
(global property)
(全局属性) - 全局对象(如window
)和它引用的对象之间的中间对象。如果使用构造函数Person
创建对象并由全局对象持有,引用路径看起来像[global]
>(global property)
> Person
。这跟一般的直接引用彼此的对象不一样。全局变量会定期修改,并且非全局变量的属性访问优化做得很好,但是不适用于全局变量。(roots)
(根) - 保留树视图中的根条目是具有对所选对象的引用的实例。它们也可以是由引擎为其自己的目的创建的引用。引擎具有引用对象的高速缓存,但是所有这样的引用都是弱引用,并且在没有强引用的情况下,不会阻止引用对象被回收。(closure)
(闭包) - 通过函数闭包引用的一组对象。(array, string, number, regexp)
- 对象类型的列表 , 这些对象的属性引用 Array,String,Number或正则表达式。
(compiled code)
(编译代码) - 简单地说,一切都与编译代码有关。脚本与函数类似,但对应于<script>
正文。SharedFunctionInfos(SFI)是位于函数和编译代码之间的对象。函数通常有一个上下文,而SFI没有。
HTMLDivElement,HTMLAnchorElement,DocumentFragment
等 - 你代码中,一个特定引用类型对elements或document对象的引用。
这个视图用于比较不同的快照,这样,你就可以通过比较它们的不同之处来找出出现内存泄露的对象。想要弄清楚一个特定的程序是否造成了泄露(比如,通常是相对的两个操作,就像是打开文档,然后关闭它,是不会留下内存垃圾的),你可以尝试下列步骤:
在比较视图中,两份快照间的不同之处会展示出来。当展开一个总入口时,添加以及删除的对象实例会显示出来:
包含视图本质上就像是你的应用程序对象结构的俯视图。它使你能够查看到函数闭包内部,甚至是观察到那些组成 JavaScript 对象的虚拟机内部对象,借助该视图,你可以了解到你的应用底层占用了多少内存。
这个视图提供了多个接入点:
为函数命名有助于你在快照中分辨不同的闭包。举个例子,下面这个函数没有命名:
function createLargeClosure() { var largeStr = new Array(1000000).join('x'); var lC = function() { // this is NOT a named function return largeStr; }; return lC; }
而下面这个是命名后的函数:
function createLargeClosure() { var largeStr = new Array(1000000).join('x'); var lC = function lC() { // this IS a named function return largeStr; }; return lC; }
Dominators
(支配者)视图显示了堆图的支配树,从形式上来看,支配者视图有点像是包含视图,但是缺少了某些属性。这是因为支配者对象可能会缺少对它的直接引用,也就是说,支配树不是生成树。
示例:尝试一下这个例子来分析闭包对内存的影响。你可能会对下面这个例子感兴趣,它可以让你深入了解堆内存分配
对象的属性以及属性值属于不同类型并且有着相应的颜色。每个属性都会有四种类型之一:
a:property
- 有名称的常规属性,通过 .
(点)操作符或者 []
(方括号)符号来访问,例如 ["foo bar"];0:element
- 有数字下标的常规属性,使用 []
(方括号)来访问。a:context var
- 函数上下文中的某个变量,在相应的函数闭包中使用其名字就可以访问。a:system prop
- 由 JavaScript 虚拟机添加的属性,在 JavaScript 代码中无法访问。被命名为 System
这样的对象是没有相应的 JavaScript 类型的。他们是 JavaScript 虚拟机的对象系统的一部分。V8 将大多数内部对象分配到和用户 JS 对象相同的堆中,所以这些都只是 V8 内部内容。
要在堆中找到某个对象,你可以使用Ctrl+F来打开搜索框,然后输入对象的 ID。
该工具的一大特点就是它能够显示浏览器本地对象(DOM 结点,CSS 规则)以及 JavaScript 对象间的双向依赖关系。这有助于发现因为忘记分离 DOM 子树而导致的不可见的泄露。
DOM 泄露肯能比你想象中的要多。考虑下面这个例子 - 什么时候 #tree
会被回收?
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
#leaf
包含了对其父亲(父节点)的引用并递归到 #tree
,所以只有当 leafRef
失效的时候 #tree
下的整棵树才能被回收。
示例: 尝试这个例子有助于你理解 DOM 节点中哪里容易出现泄露以及如何找到它们。你也可以继续尝试后面这个例子DOM 泄露比想象的要更多。
示例: 尝试这个示例来体验`detached`(分离)的 DOM 树。
推荐手册