最近发现一篇关于使用Chrome进行调试和优化的文章,写的特别全面和友好,虽然Chrome版本比较老了,但是和现在的功能基本没有大变化,还是非常值得参考的。下面是原作者的连接,但是已经打不开了
,http://www.kazaff.me/2014/01/26/chrome-developer-tools之内存分析/
-------转载分割线-------
前面的几篇中分别介绍了DevTools的不同面板,这次来看一下Profiles面板,如下图:
工具栏中的按钮功能都比较明显,除了视图模式选择框一开始难以理解。 在Profiles面板的右边列表区域中,Function列显示的影响性能的函数集合~ 而Bottom Up允许你查看指定函数的被调用轨迹(自底向上,类似异常错误提示信息中的堆栈信息)。 Top Down视图则显示函数调用深度轨迹(自上而下,类似单步调试时的step in)。
在“隐身模式”的窗口下打开上面的“output”标签卡,然后打开DevTools的Profiles面板,点击面板中的“Start”按钮,然后点击页面上的“测试按钮”,然后点击面板中的“Stop”按钮,在创建的“Profile1”中可以看到收集的信息,如下图:
Profile面板就是这么简单,我们接下来的关注点在如何查找js中的“内存泄露”或定为“内存膨胀”的原因! 在javascript中产生内存泄露的原因和其他语言也非常类似,一般是因为代码出于某种问题而没有及时释放申请到的内存而造成的!这个“泄露”过程是通常是持续渐进的,一点一点的消耗完计算机的内存资源~~对于那些基于js框架实现的SPA架构的系统,内存泄露将会是一个非常值得重视的关注点!
说到这里,就不得不提到V8的垃圾回收机制~(http://newhtml.net/v8-garbage-collection/)即便JS拥有自动回收机制,合理的内存管理还是非常有必要的!通常情况下,当你解决内存泄露问题时,请先问一下你自己下面这三个问题:
- 我的页面消耗了太多内存么? DevTools工具的Timeline面板能帮你分析提供数据支撑,Memory视图能显示出当前页面的Dom节点数,JS事件监听器个数等(性能优化的首要准则:避免持续引用太多DOM元素,注销掉过期的事件监听器,不要用全局变量存储你不需要的数据!)
- 我的页面存在内存泄露么? 对象构造追踪器能帮你缩小内存泄露的排查范围,它会实时监控JS中对象的构造情况,你可以使用“heap profiler”来记录JS的堆信息快照,通过分析和比对多张快照来定位哪些对象并没有被垃圾回收释放!
- 我的页面垃圾回收的频率是多少? 如果页面垃圾回收太频繁,那可能说明你的代码创建对象太频繁了,Timeline面板的Memory视图可以帮助你锁定频繁创建对象的代码位置!
术语和原理
对象大小 这里的对象,包括了JS中的基础类型(整型,字符串)和对象类型~ 一个对象有两种形式来持有内存:
- 直接拥有
- 间接引用
这两种形式分别对应着DevTools的Profiles面板中Heap Profiler的Shallow Size 和 Retained Size,看下图:
Shallow Size 这一列代表对象直接持有的内存大小。一个标准的JS对象通常会持有用于描述自身逻辑和存储直接值(属性值)的内存。 通常情况下应该只有字符串和数组类型可能拥有一个较大的Shallow Size。
Retained Size 这一列代表着只有当前对象所引用的其他对象占用的内存大小(稍微有点绕口,这已经我能翻译出来的最简单的解释了)! 官方文档中在描述这部分定义的时候提到了GC roots,但是同时也表明:这玩意儿并不需要开发者去关心! 按照我粗俗的理解,应该表达的是能够用于计算对象引用计数时追溯到的最初的那个根节点,我们先继续往后看。
Object’s Retaining Tree 在JS申请的堆内存中,对象之间的复杂关系会生成一张关系网,如下图:
构成这张关系网的元素有两种:
- Nodes:节点,对应一个对象,用创建该对象的构造方法来命名
- Edges:连接线,对应着对象间的引用关系,用对象属性名来命名
你可能注意到上面的快照截图中有一列叫:Distance,它表示当前对象到GC root的距离。 如果同类型的对象几乎都拥有相同的距离值,只有其中个别几个对象的距离值很大,那么你就要特别留意了!
Dominators 统治者对象拥有一棵树,树中的节点是该统治对象能完全控制的对象集合,如下图:
图中:
- 节点1完全控制节点2
- 节点2完全控制节点3,节点4,节点6
- 节点3完全控制节点5
- 节点5完全控制节点8
- 节点6完全控制节点7
这就引出一条规则:如果节点B存在于每一条从根节点到节点A的路径当中,那么我们可以称节点B是节点A的统治节点!
V8特性
现在我们来说一下V8的虚拟机中和内存有关的一些特性,了解这些有利于我们分析问题和看懂heap快照!
JavaScript对象 JS中有三种基础类型:
- Numbers
- Booleans
- Strings
其中,Numbers会以下面两种方式来存储:
- small integers(SMIs):31位内的整数值
- heap numbers:超过SMI表大小的值,例如doubles
Strings也会对应两种存储方案:
- VM heap
- 非VM heap
一个JS对象会从JS的堆内存(VM heap)中申请自己所需要的内存,而V8的垃圾回收器会在该对象不在活跃(没有任何对它的强引用后)后回收内存。
本地对象(Native objects)代表那些不在JS堆内存中的对象集合,它不受控于V8的垃圾回收机制~
Chrome的任务管理器 你可以通过“Shift” “Esc”开启Chrome任务管理器,它能让你了解当前浏览器的一些情况,包括内存使用率,特别是能查看JS的内存消耗,如下图:
当然这个方法还是过于粗糙,回想前几篇介绍DevTools的文章,我们可以回忆起在Timeline面板中有一个Memory视图,我们来看一下如何使用它来判别页面中的内存泄露!
通常情况下,为了发现并修复内存泄露问题,我们必须要有能力做到以下两点:
- 可以重建泄露的场景(或者叫操作序列)
- 基准(baseline)
前者是为了定位问题点,后者是为了验证是否修复!
通常情况下,当你准备的操作序列执行完毕后点击工具条中的垃圾箱图标(启动浏览器的垃圾回收)时,如果发现相关的资源并没有回归基准状态,那通常意味着你的代码出现了内存泄露~
一旦你确定存在内存泄露,就可以利用heap profiler来继续深究了!
打开Profiles面板,选择“Take Heap Snapshot”并点击“Start”按钮,如下图:
需要注意的是,每次你创建快照,都会自动触发一次垃圾回收~~
下面主要解释一下snapshot视图,快照可以按照不同的视角来展示:
- Summary:按照构造方法名称来分组显示对象
- Comparison:显示两个不同快照的差异
- Containment:允许查看堆内容
- Dominators:显示统治者树(没有上面那张gif图那么直观啊~这不是坑爹么?)
PS:Dominators视图默认没有开启,需要在Settings里选择“Show advanced heap snapshot”,并重启浏览器~~
Summary视图 前面其实已经提到过该视图,包括视图中显示的个别列的含义(Distance,Shallow Size,Retained Size),我们只来说一下还没有提到过的一些地方,该视图中的“Constructor”列,是基于对象的构造方法名称来分组显示当前页面中的所有对象(之前貌似也说过~)。而“Object Count”则显示对应类型的对象个数,这应该很容易理解吧~
注意,上图中,以黄色背景标注的对象表明该对象包含指向其他对象的引用,而红色背景的对象则表示它虽然没有被直接引用,但由于它属于“detached DOM tree”的一部分,所以它也是无法被回收(可以参见下面说的Dom泄露~),“@”后面的数字代表的是该对象的唯一的ID!
Comparison视图 该视图用于比对多个快照细节,来帮助你发现它们之间的差异,进而锁定哪些对象有内存泄露! 通常情况下,你提供的用来验证内存泄露的操作序列应该是相抵消的,举个例子:开启某一个窗口,再关闭它! 这样更有利于你利用基准来判断是否发生了内存泄露! 那么实际流程应该如下:
- 打开对应的页面,在开始你的操作序列之前创建一张heap快照;
- 开始你的操作序列,例如打开一个窗口;
- 结束你的操作序列,例如关闭它;
- 创建第二张heap快照,并和第一张快照进行对比。
Containment视图 这个视图可以让你更近距离观察对象结构,允许你观察函数内部的闭包,VM内核对象等~
利用Containment视图,你可以查看到所有创建的闭包细节,利用Comparison视图来对比多张快照能直观的看到闭包的增长,使用Timeline面板的Memory视图你会得到内存增长曲线,我就不截图了……
除了闭包造成的内存泄露外,我们再来看一个DOM泄露~~ 看下图:
#leaf节点会持有指向它的父节点#li的引用(一直递归到根节点#tree),所以只要#leaf还处于活跃状态(没释放),那么整棵DOM树都不会释放。所以在进行一些DOM操作的时候一定要注意这一点啊~~ 有兴趣的童鞋可以测试一下下面这段例子:
上述内容只是从官方手册中提炼出来的,可能有错误,也可能不够精细,如果发现错误,希望能够回帖更正我~ 谢谢~