原文出处: Chrome DevTools 译文出处:DestinyXie javascript
内存泄漏是指计算机可用内存的逐渐减小。当程序持续没法释放其使用的临时内存时就会发生。JavaScript的web应用也会常常遇到在原生应用程序中出现的内存相关的问题,如泄漏和溢出,web应用也须要应对垃圾回收停顿。html
整体来讲,当你以为你遇到了内存泄漏问题时,你须要思考三个问题:html5
本小节介绍在内存分析时使用的经常使用术语,这些术语在为其它语言作内存分析的工具中也适用。这里的术语和概念用在了堆分析仪(Heap Profiler)UI工具和相关的文档中。java
这些可以帮助咱们熟悉如何有效的使用内存分析工具。若是你曾用过像Java、.NET等语言的内存分析工具的话,那么这将是一个复习。node
把内存想象成一个包含基本类型(像数字和字符串)和对象(关联数组)的图表。它可能看起来像下面这幅一系列相关联的点组成的图。git
一个对象有两种使用内存的方法:github
当你使用DevTools中的堆分析仪(Heap Profiler,用来分析内存问题的工具,在DevTools的”Profile”标签下)时,你可能会惊喜的发现一些显示各类信息的栏目。其中有两项是:直接占用内存(Shallow Size)和占用总内存(Retained Size),那它们是什么意思呢?web
这个是对象自己占用的内存。正则表达式
典型的JavaScript对象都会有保留内存用来描述这个对象和存储它的直接值。通常,只有数组和字符串会有明显的直接占用内存(Shallow Size)。但字符串和数组经常会在渲染器内存中存储主要数据部分,仅仅在JavaScript对象栈中暴露一个很小的包装对象。chrome
渲染器内存指你分析的页面在渲染的过程当中所用到的全部内存:页面自己的内存 + 页面中的JS堆用到的内存 + 页面触发的相关工做进程(workers)中的JS堆用到的内存。然而,经过阻止垃圾自动回收别的对象,一个小对象都有可能间接占用大量的内存。
一个对象一但删除后它引用的依赖对象就不能被GC根(GC root)引用到,它们所占用的内存就会被释放,一个对象占用总内存包括这些依赖对象所占用的内存。
GC根是由控制器(handles)组成的,这些控制器(不管是局部仍是全局)是在创建由build-in函数(native code)到V8引擎以外的JavaScript对象的引用时建立的。全部这些控制器都可以在堆快照的GC roots(GC根) > Handle scope 和 GC roots >Global handlers中找到。若是不深刻了解浏览器的实现原理,在这篇文章中介绍这些控制器可能会让人不能理解。GC根和控制器你都不须要过多关心。
有不少内部的GC根对用户来讲都是不重要的。从应用的角度来讲有下面几种状况:
注意:咱们推荐用户在建立堆快照时,不要在console中执行代码,也不要启用调试断点。
内存图由一个根部开始,多是浏览器的window
对象或Node.js模块Global
对象。这些对象如何被内存回收不受用户的控制。
不能被GC根遍历到的对象都将被内存回收。
注意:直接占用内存和占用总内存字段中的数据是用字节表示的。
以前咱们已经了解到,堆是由各类互相关联的对象组成的网状结构。在数字领域,这种结构被称为图或内存图。图是由边缘(edges)链接着的节点(nodes)组成的,他们都被贴了标签。
本文档的后面你将了解到如何使用堆分析仪生成快照。从下图的堆分析仪生成的快照中,咱们能看到距离(distance)这个字段:是指对象到GC根的距离。若是同一个类型的全部对象的距离都同样,而有一小部分的距离却比较大,那么就可能出了些你须要进行调查的问题了。
支配对象就像一个树结构,由于每一个对象都有一个支配者。一个对象的支配者可能不会直接引用它支配的对象,就是说,支配对象树结构不是图中的生成树。
在上图中:
在下图的例子中,节点#3
是#10
的支配者,但#7
也在每一个从GC到#10
的路经中都出现了。像这样,若是B对象在每一个从根节点到A对象的路经中都出现,那么B对象就是A对象的支配对象。
在本节,咱们将描述一些内存相关的概念,这些概念是和V8 JavaScript虚拟机(V8 VM 或VM)有关的。当分析内存时,了解这些概念对理解堆快照是有帮助的。
有三个原始类型:
它们不会引用别的值,它们只会是叶子节点或终止节点。
数字(Numbers)如下面两种方式之一被存储:
字符型数据会如下面两种方式存储:
新建立的JavaScript对象会被在JavaScript堆上(或VM堆)分配内存。这些对象由V8的垃圾回收器管理,只要还有一个强引用他们就会在内存中保留。
本地对象是全部不在JavaScript堆中的对象,与堆对象不一样的是,在它们的生命周期中,不会被V8垃圾加收器处理,只能经过JavaScript包装对象引用。
链接字符串是由一对字符串合并成的对象,是合并后的结果。链接字符串只在有须要时合并。像一链接字符串的子字符串须要被构建时。
好比:若是你链接a和b,你获得字符串(a, b)这用来表示链接的结果。若是你以后要再把这个结果与d链接,你就获得了另外一个链接字符串((a, b), d)。
数组(Arrays) – 数组是数字类型键的对象。它们在V8引擎中存储大数据量的数据时被普遍的使用。像字典这种有键-值对的对象就是用数组实现的。
一个典型的JavaScript对象能够经过两种数组类型之一的方式来存储:
若是只有少许的属性,它们会被直接存储在JavaScript对象自己中。
Map – 一种用来描述对象类型和它的结构的对象。好比,maps会被用来描述对象的结构以实现对对象属性的快速访问
每一个本地对象组都是由一组之间相互关联的对象组成的。好比一个DOM子树,每一个节点都能访问到它的父元素,下一个子元素和下一个兄弟元素,它们构成了一个关联图。须要注意的是本地元素没有在JavaScript堆中表现-这就是它们的大小是零的缘由,而它的包装对象被建立了。
每一个包装对象都会有一个到本地对象的引用,用来传递对这些本地对象的操做。这些本地对象也有到包装对象的引用。但这并不会创造没法收回的循环,GC是足够智能的,可以分辨出那些已经没有引用包装对象的本地对象并释放它们的。但若是有一个包装对象没有被释放那它将会保留全部对象组和相关的包装对象。
注意: 当使用Chrome作内存分析时,最好设置一个洁净的测试环境
打开Chrome的内存管理器,观察内存字段,在一个页面上作相关的操做,你能够很快定位这个操做是否会致使页面占用不少内存。你能够从Chrome菜单 > 工具或按Shift + Esc,找到内存管理器。
打开后,在标头右击选用 JavasScript使用的内存 这项。
解决问题的第一步就是要可以证实问题存在。这就须要建立一个可重现的测试来作为问题的基准度量。没有可再现的程序,就不能可靠的度量问题。换句话说若是没有基准来作为对比,就没法知道是哪些改变使问题出现的。
时间轴面版(Timeline panel)对于发现程序何时出了问题很用帮助。它展现了你的web应用或网站加载和交互的时刻。全部的事件:从加载资源到解JavaScript,样式计算,垃圾回收停顿和页面重绘。都在时间轴上表示出来了。
当分析内存问题时,时间轴面版上的内存视图(Memory view)能用来观察:
更多的关于在内存分析时,定位内存泄漏的方法,请阅Zack Grossbart的Memory profiling with the Chrome DevTools
首先要作的事情是找出你认为可能致使内存泄漏的一些动做。能够是发生在页面上的任何事件,鼠标移入,点击,或其它可能会致使页面性能降低的交互。
在时间轴面版上开始记录(Ctrl+E 或 Cmd+E)而后作你想要测试的动做。想要强制进行垃圾回收点面版上的垃圾筒图标()。
下面是一个内存泄漏的例子,有些点没有被垃圾回收:
若是通过一些反复测试后,你看到的是锯齿状的图形(在内存面版的上方),说明你的程序中有不少短时存在的对象。而若是一系列的动做没有让内存保持在必定的范围,而且DOM节点数没有返回到开始时的数目,你就能够怀疑有内存泄漏了。
一旦肯定了存在内存上的问题,你就可使用分析面板(Profiles panel)上的堆分析仪(heap profiler)来定位问题的来源。
例子: 尝试一下memory growth的例子,能帮助你有效的练习经过时间轴分析内存问题。
内存回收器(像V8中的)须要可以定位哪些对象是活的(live),而那些被认为是死的(垃圾)的对象是没法引用到的(unreachable)。
若是垃圾回收 (GC)由于JavaScript执行时有逻辑错误而没有可以回收到垃圾对象,这些垃圾对象就没法再被从新回收了。像这样的状况最终会让你的应用愈来愈慢。
好比你在写代码时,有的变量和事件监听器已经用不到了,可是却仍然被有些代码引用。只要引用还存在,那被引用的对象就没法被GC正确的回收。
当你的应用程序在运行中,有些DOM对象可能已经更新/移除了,要记住检查引用了DOM对象的变量并将其设null。检查可能会引用到其它对象(或其它DOM元素)的对象属性。双眼要盯着可能会愈来愈增加的变量缓存。
在Profiles面板中,选择Take Heap Snapshot,而后点击Start或者按Cmd + E或者Ctrl + E:
快照最初是保存在渲染器进程内存中的。它们被按需导入到了DevTools中,当你点击快照按钮后就能够看到它们了。当快照被载入DevTools中显示后,快照标题下面的数字显示了可以被引用到的(reachable)JavaScript对象占有内存总数。
例子:尝试一下garbage collection in action的例子,在时间轴(Timeline)面板中监控内存的使用。
注意:关闭DevTools窗口并不能从渲染内存中删除掉收集的快照。当从新打开DevTools后,以前的快照列表还在。
记住咱们以前提到的,当你生成快照时你能够强制执行在DevTools中GC。当咱们拍快照时,GC是自动执行的。在时间轴(Timeline)中点击垃圾桶(垃圾回收)按钮()就能够轻松的执行垃圾回收了。
例子:尝试一下scattered objects并用堆分析仪(Heap Profiler)分析它。你能够看到(对象)项目的集合。
一个快照能够根据不一样的任务切换视图。能够经过如图的选择框切换:
下面是三个默认视图:
Dominators(支配者)视图能够在Settings面板中开启 – 显示dominators tree. 能够用来找到内存增加点。
对象的属性和属性值有不一样的类型并自动的经过颜么进行了区分。每一个属性都是如下四种之一:
命名为System
的对象没有对应的JavaScript类型。它们是JavaScript VM对象系统内置的。V8将大多数内置对象和用户JS对象放在同一个堆中。但它们只是V8的内部对象。
打开一个快照,默认是以概要视图显示的,显示了对象总数,能够展开显示具体内容: Initially, a snapshot opens in the Summary view, displaying object totals, which can be expanded to show instances:
第一层级是”整体”行,它们显示了:
展开一个整体行后,会显示全部的对象实例。没一个实例的直接占用内存和占用总内存都被相应显示。@符号后的数字不对象的惟一ID,有了它你就能够逐个对象的在不一样快照间做对比。
例子:尝试这个例子(在新tab标签中打开)来了解如何使用概要视图。
记住黄色的对象被JavaScript引用,而红色的对象是由黄色背景色引用被分离了的节点。
该视图用来对照不一样的快照来找到快照之间的差别,来发现有内存泄漏的对象。来证实对应用的某个操做没有形成泄漏(好比:通常一对操做和撤消的动做,像找开一个document,而后关闭,这样是不会形成泄漏的),你能够按如下的步骤尝试:
在对照视图下,两个快照之间的不一样就会展示出来了。当展开一个总类目后,增长和删除了的对象就显示出来了:
例子:尝试例子(在新tab标签中打开)来了解如何使用对照视图来定位内存泄漏。
控制视图能够称做对你的应用的对象结构的”鸟瞰视图(bird’s eys view)”。它能让你查看function内部,跟你的JavaScript对象同样的观察VM内部对象,能让你在你的应用的很是低层的内存使用状况。
该视图提供了几个进入点:
下图是一个典型的控制视图:
例子:尝试例子(在新tab标签中打开)来了解如何使用控制视图来查看闭包内部和事件处理。
关于闭包的建议
给函数命名对你在快照中的闭包函数间做出区分会很用帮助。如:下面的例子中没有给函数命名:
1
2
3
4
5
6
7
8
9
|
function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function() { // this is NOT a named function
return largeStr;
};
return lC;
}
|
而下面这个有给函数命名:
1
2
3
4
5
6
7
8
9
|
function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function lC() { // this IS a named function
return largeStr;
};
return lC;
}
|
例子:尝试这个例子why eval is evil来分析内存中闭包的影响。你可能也对尝试下面这个例子,记录heap allocations(堆分配)有兴趣。
这个工具独一无二的一点是展现了浏览器原生对象(DOM节点,CSS规则)和JavaScript对象之间的双向引用。这能帮助你发现由于忘记解除引用游离的DOM子节点而致使的难以发觉的内存泄漏。
DOM内存泄漏可能会超出你的想象。看下下面的例子 – #tree对象何时被GC呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
表明了对它的父节点的引用(parentNode)它递归引用到了#tree
,因此,只有当leafRef被nullified后#tree
表明的整个树结构才会被GC回收。
例子:尝试leaking DOM nodes来了解哪里DOM节点会内存泄漏并如何定位。你也能够看一下这个例子:DOM leaks being bigger than expected。
查看Gonzalo Ruiz de Villa的文章Finding and debugging memory leaks with the Chrome DevTools来阅读更多关于DOM内存泄漏和内存分析的基础。
原生对象在Summary和Containment视呼中更容易找到 – 有它们专门的类目:
例子:尝试下这个例子(在新tab标签中打开)来了解如何将DOM树分离。
支配者视图显示了堆图的支配者树。支配者视图跟控制(Containment)视图很像,可是没有属性名。这是由于支配者可能会是一个没有直接引用的对象,就是说这个支配者树不是堆图的生成树。但这是个有用的视图能帮助咱们很快的定位内存增加点。
注意:在Chrome Canary中,支配者视图可以在DevTools中的Settings > Show advanced heap snapshot properties 开启,重启DevTools生效。
例子:尝试这个例子(在新tab标签中打开)来练习如何找到内存增加点。能够进一步尝试下一个例子retaining paths and dominators。
对象跟踪器整合了heap profiler的快照增量更新分析和Timeline面板的记录。跟其它工具同样,记录对象的堆配置须要启动记录,执行一系列操做,而后中止记录而后进行分析。
对象跟踪器不间断的记录堆快照(频率达到了每50毫秒!),结束时记录最后一个快照。该堆分配分析器显示对象在哪被建立并定位它的保留路径。
开启并使用对象分析器
开始使用对象分析器: 1. 确认你使用的是最新版的Chrome Canary。
上面的柱条表示在堆中生成的新对象。高度就对应了相应对象的大小,它的颜色表示了这个对象是否在最后拍的那个快照中还在:蓝色柱表示在timeline最后这个对象还在,灰色柱表示这个对象在timeline中生成,但结束前已经被内存回收了。
上面的例子中,一个动做执行了10次。同一个程序保留了5个对象,因此最后5个蓝色柱条被保留了。但这最后留下的柱存在潜在的问题。你能够用timeline上的滑动条缩小到那个特定的快照并找到这个分配的对象。
点击一个堆中的对象就能在堆快照的下面部分显示它的保留总内存树。检查这个对象的保留总内存树可以给你足够的信息来了解为何这个对象没有被回收,而后你就能对代码作相应的修改来去掉没必要要的引用。
问:我不能看到对象的全部属性,我也看到它们的非字符串值!为何?
并不是全部属性都完整的保存在JavaScript堆中。其中有些是经过执行原生代码的getters方法来获取的。这些属性没有在堆快照中捕获,是为了防止对getters方法的调用和避免程序状态的改变,若是这些getters方法不是”纯(pure)”的functions。一样,非字符串的值,如数字,没有被捕获是为了减小快照的大小。
问:@符号后面的数字是什么意思 – 是地址仍是ID呢?这个ID值真的是惟一的么?
这是对象ID。显示对象的地址没有意义,由于一个对象会在垃圾回收的时候被移除。这些对象IDs是真正的IDs – 就是说,它们在不一样的快照间是惟一表示的。这样就能够的堆状态间进行精确的对比。维持这些IDs会给GC流程增长额外的开支,但这仅在记录第一次堆快照时分配 – 若是堆分析仪没有用到,就不会有额外的开支。
问:”死”(没法引用到的)对象被包含在快照中了么?
没有,只有能够引用到的对象才会显示在快照中。并且,拍快照前都会先自动执行GC操做。
注意:在写这篇文章的时候,咱们计划在拍快照的时候再也不GC,防止堆尺寸的减小。如今已是这样了,但垃圾对象依然显示在快照以外。
问:GC根是由什么组成的?
由不少部分组成:
问:我得知可使用Heap Profiler和Timeline Memory view来检测内存泄漏。但我应该先用哪一个工具呢?
Timeline面版,是在你第一次使用你的页面发现速度变慢了时用来论断过多的内存使用。网站变慢是比较典型的内存泄漏的信号,但也多是其它的缘由 – 多是有渲染或网络传输方面的瓶颈,因此要确保解决你网页的真正问题。
论断是不是内存问题,就打开Timeline面板和Memory标签。点击record按钮,而后在你的应用上重复几回你认为可能致使内存泄漏的操做。中止记录。你应用的内存使用图就生成出来了。若是内存的使用一直在增加(而没有相应的降低),这就代表你的应用可能有内存泄漏了。
通常一个正常的应用的内存使用图形是锯齿状的,由于内存使用后又会被垃圾回收器回收。不用担忧这种锯齿形 – 由于老是会由于JavaScript而有内存的消耗,甚至一个空的requestAnimationFrame
也会形成这种锯齿形,这是没法避免的。只要不是那种分配了持续不少内存的形状,那就代表生成了不少内存垃圾。
上图的增加线是须要你警戒的。在诊断分析的时候Memory标签中的DOM node counter,Document counter和Event listener count也是颇有用的。DOM节点数是使用的原生内存不会影响JavaScript内存图。
一旦你确认你的应用有内存泄漏,堆分析仪就能够用来找到内存泄漏的地方。
问:我发现堆快照中有的DOM节点的数字是用红色标记为”Detached DOM tree”,而其它的是黄色的,这是什么意思呢?
你会发现有不一样的颜色。红色的节点(有着深色的背景)没有从JavaScript到它们的直接的引用,但它们是分离出来的DOM结构的一部分,因此他们仍是在内存中保留了。有可能有一个节点被JavaScript引用到了(多是在闭包中或者一个变量),这个引用会阻止整个DOM树被内存回收。
黄色节点(黄色背景)有JavaScript的直接引用。在同一个分离的DOM树中查看一个黄色的节点来定位你的JavaScript的引用。就可能看到从DOM window到那个节点的属性引用链(如:window.foo.bar[2].baz
)。
下面的动态图显示了分离节点的处理过程:
例子:尝试这个例子detached nodes你能够查看节点在Timeline中的生命周期,而后拍堆快照来找到分离的节点。
问:直接占用内存(Shallow Size)和占用总内存(Retained Size)分别表明什么,它们的区别是什么?
是这样的,对象能够在内存中以两种方式存在(be alive) – 直接的被别一个可访问的(alive)对象保留(window和document对象老是可访问的)或被原生对象(象DOM对象)隐含的包留引用。后一种方式会由于阻止对象被GC自动回收,而有导制内存泄泥漏的可能。对象自身占用的内存被称为直接占用内存(一般来讲,数组和字符串会保留更多的直接占用内存(shallow size))。
一个任意大小的对象能够经过阻止其它对象内存被回收在保留很大的内存使用。当一个对象被删除后(它形成的一些依赖就没法被引用了)可以释放的内存的大小被称有占用总内存(retained size)。
问:constructor和retained字段下有不少的数据。我应该从哪开始调查我是的否遇到了内存泄漏呢?
通常来讲最好是从经过retainers排序的第一个对象开始,retainers之间是经过距离排序的(是指到window对象的距离)。
距离最短的对象有多是首选的可能致使内存泄漏的对象。
问:Summary, Comparison, Dominators 和 Containment这些视图之间的不一样是什么?
你能够经过切换视图来体验它们的区别。
问:堆分析仪中的constructor(一组)内容表明什么?
在你的程序的生命周期中生成的不少其它的对象,包括事件监听器或自定义对象,能够在下面的controllers中找到:
问:我在作内存分析时须要关闭Chrome里可能会产生影响的什么功能么?
咱们建议在用Chrome DevTools作内存分析时,你可使用关闭全部扩展功能的隐身模式,或设置用户文件夹为(--user-data-dir=""
)后再打开Chrome。
应用,扩展甚至console中的记录都会对你的分析有潜在的影响,若是你想让你的分析可靠的话,禁用这些吧。
写在最后的话
今天的JavaScript引擎已经具备很强的能力,可以自动回收代码产生的内存垃圾。就是说,它们只能作到这样了,但咱们的应用仍然被证实会由于逻辑错误而产生内存泄漏。使用相应的工具来找到应用的瓶颈,记住,不要靠猜 – 测试它。
尽管不少内容在本文章中已经提到了,但一系列测试内存相关的问题的例子仍是颇有用的,下面是一组DOM节点内存泄漏的例子。你可能但愿在测试你的更复杂的页面或应用前先用这些例子作试验。
更多例子: