如何录制堆快照
学习如何用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?)点。此视图可以帮助确认对象在删除/垃圾回收之前是否有外部引用任在工作。
视图之间进行切换,请使用视图底部的下拉框:
注意: 并非所有属性都存储在JavaScript堆上。 不捕获使用执行本地代码的getter实现的属性。此外,不捕获非字符串值(如数字)。摘要视图(Summary view)
最开始的时候,快照是在总结视图中打开的,显示了对象的整体情况,并且该视图可以展开以显示实例信息:
顶级入口是 “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 对象的虚拟机内部对象,借助该视图,你可以了解到你的应用底层占用了多少内存。
这个视图提供了多个接入点:
- DOMWindow objects - 这些是被认作“全局”对象的对象。
- GC roots - 虚拟机垃圾回收器实际实用的垃圾回收根节点。
- Native objects - 指的是“推送”到 JavaScript 虚拟机内以实现自动化的浏览器对象,比如,DOM 节点,CSS 规则。
关于闭包的小提示
为函数命名有助于你在快照中分辨不同的闭包。举个例子,下面这个函数没有命名:
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 内存泄露
该工具的一大特点就是它能够显示浏览器本地对象(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 树。