本文转自JS内存泄漏排查方法——Chrome Profiles

概述

Google Chrome浏览器提供了非常强大的JS调试工具,Heap Profiling便是其中一个。Heap Profiling可以记录当前的堆内存(heap)快照,并生成对象的描述文件,该描述文件给出了当时JS运行所用到的所有对象,以及这些对象所占用的内存大小、引用的层级关系等等。这些描述文件为内存泄漏的排查提供了非常有用的信息。

注意:本文里的所有例子均基于Google Chrome浏览器。

什么是heap

JS运行的时候,会有栈内存(stack)和堆内存(heap),当我们用new实例化一个类的时候,这个new出来的对象就保存在heap里面,而这个对象的引用则存储在stack里。程序通过stack里的引用找到这个对象。例如var a = [1,2,3];,a是存储在stack里的引用,heap里存储着内容为[1,2,3]的Array对象。

Heap Profiling

打开工具

打开Chrome浏览器(版本25.0.1364.152 m),打开要监视的网站(这里以游戏大厅为例),按下F12调出调试工具,点击“Profiles”标签。可以看到下图:

可以看到,该面板可以监控CPU、CSS和内存,选中“Take Heap Snapshot”,点击“Start”按钮,就可以拍下当前JS的heap快照,如下图所示:

右边视图列出了heap里的对象列表。由于游戏大厅使用了Quark游戏库,所以这里可以清楚地看到Quark.XXX之类的类名称(即Function对象的引用名称)。

注意:每次拍快照前,都会先自动执行一次GC,所以在视图里的对象都是可及的。

视图解释

列字段解释:

Constructor — 类名Distance — 估计是对象到根的引用层级距离
Objects Count — 给出了当前有多少个该类的对象
Shallow Size — 对象所占内存(不包含内部引用的其它对象所占的内存)(单位:字节)
Retained Size — 对象所占总内存(包含内部引用的其它对象所占的内存)(单位:字节)
下面解释一下部分类名称所代表的意思:

(compiled code) — 未知,估计是程序代码区
(closure) — 闭包(array) — 未知
Object — JS对象类型(system) — 未知
(string) — 字符串类型,有时对象里添加了新属性,属性的名称也会出现在这里
Array — JS数组类型cls — 游戏大厅特有的继承类
Window — JS的window对象
Quark.DisplayObjectContainer — Quark引擎的显示容器类
Quark.ImageContainer — Quark引擎的图片类
Quark.Text — Quark引擎的文本类
Quark.ToggleButton — Quark引擎的开关按钮类
对于cls这个类名,是由于游戏大厅的继承机制里会使用“cls”这个引用名称,指向新建的继承类,所以凡是使用了该继承机制的类实例化出来的对象,都放在这里。例如程序中有一个类ClassA,继承了Quark.Text,则new出来的对象是放在cls里,不是放在Quark.Text里。

查看对象内容

点击类名左边的三角形,可以看到所有该类的对象。对象后面的“@70035”表示的是该对象的ID(有人会错认为是内存地址,GC执行后,内存地址是会变的,但对象ID不会)。把鼠标停留在某一个对象上,会显示出该对象的内部属性和当时的值。

这个视图有助于我们辨别这是哪个对象。但该视图跟踪不了是被谁引用了。

查看对象的引用关系

点击其中一个对象,能看到对象的引用层级关系,如下图:

Object’s retaining tree视图显示出了该对象被哪些对象引用了,以及这个引用的名称。图中的这个对象被5个对象引用了,分别是:

一个cls对象的 _txtContent 变量;
一个闭包函数的context变量;
同一个闭包函数的self变量;
一个数组对象的0位置;
一个Quark.Tween对象的target变量。
看到context和self这两个引用,可以知道这个Quark.Text对象使用了JS常用的上下文绑定机制,被一个闭包里的变量引用着,相当于该Quark.Text对象多了两个引用,这种情况比较容易出现内存泄漏,如果闭包函数不释放,这个Quark.Text对象也释放不了。

展开_textContent,可以看到下一级的引用:

把这个树状图反过来看,可以看到,该对象(ID @70035)其中的一条引用链是这样的:

GameListV _curV _gameListV 省略…
\ | /
\ | /
_noticeWidget
|
_noticeC
|
_noticeV
|
_txtContent
||
Quark.Text @70035
内存快照的对比通过快照对比的功能,可以知道程序在运行期间哪些对象变更了。

刚才已经拍下了一个快照,接下来再拍一次,如下图:

点击图中的黑色实心圆圈按钮,即可得到第二个内存快照:

然后点击图中的“Snapshot 2”,视图才会切换到第二次拍的快照。

点击图中的“Summary”,可弹出一个列表,选择“Comparison”选项,结果如下图:

这个视图列出了当前视图与上一个视图的对象差异。列名字段解释:# New — 新建了多少个对象# Deleted — 回收了多少个对象# Delta — 对象变化值,即新建的对象个数减去回收了的对象个数Size Delta — 变化的内存大小(字节)注意Delta字段,尤其是值大于0的对象。下面以Quark.Tween为例子,展开该对象,可看到如下图所示:

在“# New”列里,如果有“.”,则表示是新建的对象。

在“# Deleted”列里,如果有“.”,则表示是回收了的对象。

平时排查问题的时候,应该多拍几次快照进行对比,这样有利于找出其中的规律。

内存泄漏的排查

JS程序的内存溢出后,会使某一段函数体永远失效(取决于当时的JS代码运行到哪一个函数),通常表现为程序突然卡死或程序出现异常。

这时我们就要对该JS程序进行内存泄漏的排查,找出哪些对象所占用的内存没有释放。这些对象通常都是开发者以为释放掉了,但事实上仍被某个闭包引用着,或者放在某个数组里面。

观察者模式引起的内存泄漏

有时我们需要在程序中加入观察者模式(Observer)来解藕一些模块,但如果使用不当,也会带来内存泄漏的问题。

排查这类型的内存泄漏问题,主要重点关注被引用的对象类型是闭包(closure)和数组Array的对象。

下面以德州扑克游戏为例:

测试人员发现德州扑克游戏存在内存溢出的问题,重现步骤:进入游戏–退出到分区–再进入游戏–再退出到分区,如此反复几次便出现游戏卡死的问题。

排查的步骤如下:

打开游戏;
进入第一个分区(快速场5/10);
进入后,拍下内存快照;
退出到刚才的分区界面;
再次进入同一个分区;
进入后,再次拍下内存快照;
重复步骤2到6,直到拍下5组内存快照;
将每组的视图都转换到Comparison对比视图;
进行内存对比分析。
经过上面的步骤后,可以得到下图结果:

先看最后一个快照,可以看到闭包(closure)+1,这是需要重点关注的部分。(string)、(system)和(compiled code)类型可以不管,因为提供的信息不多。

接着点击倒数第二个快照,看到闭包(closure)类型也是+1。

接着再看上一个快照,闭包还是+1。

这说明每次进入游戏都会创建这个闭包函数,并且退出到分区的时候没有销毁。

展开(closure),可以看到非常多的function对象:

建新的闭包数量是49个,回收的闭包数量是48个,即是说这次操作有48个闭包正确释放了,有一个忘记释放了。每个新建和回收的function对象的ID都不一样,找不到任何的关联性,无法定位是哪一个闭包函数出了问题。

接下来打开Object’s retaining tree视图,查找引用里是否存在不断增大的数组。

如下图,展开“Snapshot 5”每个function对象的引用:

其中有个function对象的引用deleFunc存放在一个数组里,下标是4,数组的对象ID是@45599。

继续查找“Snapshot 4”的function对象:

发现这里有一个function的引用名称也是deleFunc,也存放在ID为@45599的数组里,下标是3。这个对象极有可能是没有释放掉的闭包。

继续查看“Snapshot 3”里的function对象:

从图中可以看到同一个function对象,下标是2。那么这里一定存在内存泄漏问题。

数组下面有一个引用名称“login_success”,在程序里搜索一下该关键字,终于定位到有问题的代码。因为进入游戏的时候注册了“login_success”通知:

ob.addListener(“login_success”, _onLoginSuc);
但退出到分区的时候,没有移除该通知,下次进入游戏的时候,又再注册了一次,所以造成function不断增加。改成退出到分区的时候移除该通知:

ob.removeListener(“login_success”, _onLoginSuc);
这样就成功解决这个内存泄漏的问题了。

德州扑克这种问题多数见于观察者设计模式中,使用一个全局数组存储所有注册的通知,如果忘记移除通知,则该数组会不断增大,最终造成内存溢出。

上下文绑定引起的内存泄漏

很多时候我们会用到上下文绑定函数bind(也有些人写成delegate),无论是自己实现的bind方法还是JS原生的bind方法,都会有内存泄漏的隐患。

下面举一个简单的例子:

<script type="text/javascript">var ClassA = function(name){this.name = name;this.func = null;};
            <span class="hljs-keyword">var</span> a = <span class="hljs-keyword">new</span> ClassA(<span class="hljs-string">"a"</span>);<span class="hljs-keyword">var</span> b = <span class="hljs-keyword">new</span> ClassA(<span class="hljs-string">"b"</span>);b.func = bind(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span>{<!-- --></span>console.log(<span class="hljs-string">"I am "</span> + <span class="hljs-keyword">this</span>.name);}, a);b.func();  <span class="hljs-comment">//输出 I am a</span>a = <span class="hljs-literal">null</span>;        <span class="hljs-comment">//释放a</span><span class="hljs-comment">//b = null;        //释放b</span><span class="hljs-comment">//模拟上下文绑定</span><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bind</span><span class="hljs-params">(func, self)</span>{<!-- --></span><span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span>{<!-- --></span><span class="hljs-keyword">return</span> func.apply(self);};};

上面的代码中,bind通过闭包来保存上下文self,使得事件b.func里的this指向的是a,而不是b。

首先我们把b = null;注释掉,只释放a。看一下内存快照:

可以看到有两个ClassA对象,这与我们的本意不相符,我们释放了a,应该只存在一个ClassA对象b才对。

从上面两个图可以看出这两个对象中,一个是b,另一个并不是a,因为a这个引用已经置空了。第二个ClassA对象是bind里的闭包的上下文self,self与a引用同一个对象。虽然a释放了,但由于b没有释放,或者b.func没有释放,使得闭包里的self也一直存在。要释放self,可以执行b=null或者b.func=null。

把代码改成:

<script type="text/javascript">var ClassA = function(name){this.name = name;this.func = null;};
            <span class="hljs-keyword">var</span> a = <span class="hljs-keyword">new</span> ClassA(<span class="hljs-string">"a"</span>);<span class="hljs-keyword">var</span> b = <span class="hljs-keyword">new</span> ClassA(<span class="hljs-string">"b"</span>);b.func = bind(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span>{<!-- --></span>console.log(<span class="hljs-string">"I am "</span> + <span class="hljs-keyword">this</span>.name);}, a);b.func();        <span class="hljs-comment">//输出 I am a</span>a = <span class="hljs-literal">null</span>;        <span class="hljs-comment">//释放a</span>b.func = <span class="hljs-literal">null</span>;        <span class="hljs-comment">//释放self</span><span class="hljs-comment">//模拟上下文绑定</span><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bind</span><span class="hljs-params">(func, self)</span>{<!-- --></span><span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span>{<!-- --></span><span class="hljs-keyword">return</span> func.apply(self);};};

再看看内存:

可以看到只剩下一个ClassA对象b了,a已被释放掉了。

结语

JS的灵活性既是优点也是缺点,平时写代码时要注意内存泄漏的问题。当代码量非常庞大的时候,就不能仅靠复查代码来排查问题,必须要有一些监控对比工具来协助排查。

之前排查内存泄漏问题的时候,总结出以下几种常见的情况:

闭包上下文绑定后没有释放;
观察者模式在添加通知后,没有及时清理掉;
定时器的处理函数没有及时释放,没有调用clearInterval方法;
视图层有些控件重复添加,没有移除。

JS内存泄漏排查方法——Chrome Profiles相关推荐

  1. JS内存泄漏排查方法(Chrome Profiles)

    一.概述  Google Chrome浏览器提供了非常强大的JS调试工具,Heap Profiling便是其中一个.Heap Profiling可以记录当前的堆内存(heap)快照,并生成对象的描述文 ...

  2. iOS 内存泄漏排查方法及原因分析

    级别: ★★☆☆☆ 标签:「iOS」「内存泄漏排查」「Leaks工具」 作者: MrLiuQ 审校: QiShare团队 本文将从以下两个层面解决iOS内存泄漏问题: 内存泄漏排查方法(工具) 内存泄 ...

  3. 哪些操作会造成内存泄漏及Js内存泄露解决方法

    1.垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量.如果一个对象的 引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的 内存即可回收 2.setTim ...

  4. iview select 内存泄漏_Vue遇到的内存泄漏排查处理

    Vue遇到的内存泄漏排查处理 1.定位问题跟踪具体那一部分造成的泄漏. (1)js写法(闭包.全局变量等).dom事件监听.循环定时器等这些造成的泄漏在度娘上应该都很好找到处理: (2)组件的泄漏(D ...

  5. 一次恐怖的 Java 内存泄漏排查实战

    转载自  一次恐怖的 Java 内存泄漏排查实战 最近在看<深入理解Java虚拟机:JVM高级特性与最佳实践>(第二版)这本书,理论+实践结合,深入浅出,强烈推荐给大家. 这两天对JVM内 ...

  6. 填坑总结:python内存泄漏排查小技巧

    摘要:最近服务遇到了内存泄漏问题,运维同学紧急呼叫解决,于是在解决问题之余也系统记录了下内存泄漏问题的常见解决思路. 本文分享自华为云社区<python内存泄漏排查小技巧>,作者:luti ...

  7. 使用 .Net Memory Profiler 诊断 .NET 应用内存泄漏(方法与实践)

    使用 .Net Memory Profiler 诊断 .NET 应用内存泄漏(方法与实践) 博客分类: Troubleshooting & tuning .netASP.netLoadrunn ...

  8. 异常连接导致的内存泄漏排查

    目录 异常连接导致的内存泄漏排查 背景 详细流程 使用windbg分析dump文件 使用wireshark抓包分析 完成端口和重叠IO 重叠I/O 完成端口 Reactor模型与Proactor模型 ...

  9. js的event loop/js内存泄漏

    js的event-loop机制 event-loop主要有三部分组成 执行栈,消息队列.微任务队列 执行优先级是 执行栈>微任务队列>消息对系列 js是单线程语言,event loop开始 ...

最新文章

  1. Python编程基础:第四十五节 方法链Method Chaining
  2. 《堡垒之夜》中你可能没注意到的设计
  3. oracle 9i从入门到精通读书笔记2
  4. sql查询无结果返回空_3分钟短文 | Laravel 查询结果检查是不是空,5个方法你别用错...
  5. php trace 图形,PHP Trace 设计原理
  6. 毕业五年同是程序员为什么差距这么大?他年薪百万,他月薪一万
  7. 佳能MP258mp259清零软件
  8. 阿里云移动推送iOS
  9. android 正三角,倒三角的实现代码
  10. 华为交换机不同VLAN间通信的两种主流解决方案,一分钟快速掌握
  11. Large Scale Spectral Clustering with Landmark-Based Representation
  12. 全景故宫--全景图片展现数字故宫
  13. alpine 组件安装
  14. 应广单片机定时器中断配置
  15. Linux内核异常调试工具与方法
  16. android+p开机动画,Android开机动画bootanimation.zip文件制作以及注意事项
  17. 《前端》慕课--分页导航(带页码的分页导航)
  18. Android 免费短信验证码--Mob.com
  19. 限定学校|在站博士后省公派新加坡国立大学从事博后研究
  20. 鸿蒙能超越苹果系统吗,任正非说,鸿蒙与苹果系统相媲美应该不需要两到三年!鸿蒙真的已经这么完善了吗?...

热门文章

  1. 数据库:DML语言和DDL语言
  2. STM32CUBEMX开发GD32F303(14)----IIC之配置OLED
  3. 心田花开|写作技巧,人物心理描写八大方式
  4. ppt翻译效率不高,快试试这几种方法轻松翻译
  5. Shell 编程学习资料
  6. php如何输出反斜杠,php输出反斜杠的实例方法
  7. 【专访邹欣】投身软件工程教育的程序员
  8. 代谢组学喜讯|百趣生物与金域医学达成代谢组学战略合作
  9. IntelliJ IDEA 12详细开发教程(一)思想的转变与新手入门
  10. 保留小数点1位 php,PHP保留小数位的三种方法