原文网址:http://blog.csdn.net/kaitiren/article/details/19974269

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

 
Google Chrome浏览器提供了非常强大的JS调试工具,Heap Profiling便是其中一个。Heap Profiling可以记录当前的堆内存(heap)快照,并生成对象的描述文件,该描述文件给出了当时JS运行所用到的所有对象,以及这些对 ...
 
 
一、概述
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个对象引用了,分别是:
1. 一个cls对象的_txtContent变量;
2. 一个闭包函数的context变量;
3. 同一个闭包函数的self变量;
4. 一个数组对象的0位置;
5. 一个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的对象。
下面以德州扑克游戏为例:
测试人员发现德州扑克游戏存在内存溢出的问题,重现步骤:进入游戏--退出到分区--再进入游戏--再退出到分区,如此反复几次便出现游戏卡死的问题。
排查的步骤如下:
1.打开游戏;
2.进入第一个分区(快速场5/10);
3.进入后,拍下内存快照;
4.退出到刚才的分区界面;
5.再次进入同一个分区;
6.进入后,再次拍下内存快照;
7.重复步骤2到6,直到拍下5组内存快照;
8.将每组的视图都转换到Comparison对比视图;
9.进行内存对比分析。
经过上面的步骤后,可以得到下图结果:
先看最后一个快照,可以看到闭包(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;
                };

                var a = new ClassA("a");
                var b = new ClassA("b");

                b.func = bind(function(){
                        console.log("I am " + this.name);
                }, a);

                b.func();  //输出 I am a

                a = null;        //释放a
                //b = null;        //释放b

                //模拟上下文绑定
                function bind(func, self){
                        return function(){
                                return func.apply(self);
                        };
                }; 
</script>
上面的代码中,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;
                };
                var a = new ClassA("a");
                var b = new ClassA("b");
                b.func = bind(function(){
                        console.log("I am " + this.name);
                }, a);
                b.func();        //输出 I am a
                a = null;        //释放a
                b.func = null;        //释放self
                //模拟上下文绑定
                function bind(func, self){
                        return function(){
                                return func.apply(self);
                        };
                };
</script>
再看看内存:
可以看到只剩下一个ClassA对象b了,a已被释放掉了。
四、结语
JS的灵活性既是优点也是缺点,平时写代码时要注意内存泄漏的问题。当代码量非常庞大的时候,就不能仅靠复查代码来排查问题,必须要有一些监控对比工具来协助排查。
之前排查内存泄漏问题的时候,总结出以下几种常见的情况:
1.闭包上下文绑定后没有释放;
2.观察者模式在添加通知后,没有及时清理掉;
3.定时器的处理函数没有及时释放,没有调用clearInterval方法;
4.视图层有些控件重复添加,没有移除。

Chrome JS内存泄漏排查方法(Chrome Profiles)的更多相关文章

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

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

  2. JS内存泄漏排查方法-Chrome Profiles

    原文链接:http://caibaojian.com/chrome-profiles.html 一.概述 Google Chrome浏览器提供了非常强大的JS调试工具,Heap Profiling便是 ...

  3. windows 下面的内存泄漏排查.

    内存泄漏排查 一下本人只是简单的介绍一个实用, 如果读者很感兴趣, 可以查阅msdn自己去深入调查相关的API和原理. API 介绍 1. 马上打印泄漏信息:_CrtDumpMemoryLeaks() ...

  4. iOS AFNetworking内存泄漏处理方法

    iOS AFN内存泄漏处理方法 细心的你是否也发现AFN的内存泄漏的问题了呢. 在这里给大家提供一个解决AFN内存泄漏的方法. 单例解决AFN内存泄漏 + (AFHTTPSessionManager ...

  5. C++程序内存泄漏检测方法

    一.前言 在Linux平台上有valgrind可以非常方便的帮助我们定位内存泄漏,因为Linux在开发领域的使用场景大多是跑服务器,再加上它的开源属性,相对而言,处理问题容易形成“统一”的标准.而在W ...

  6. VS2005内存泄漏检测方法[转载]

    一.非MFC程序可以用以下方法检测内存泄露: 1. 程序开始包含如下定义: #ifdef _DEBUG #define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __ ...

  7. Android内存管理(5)*官方教程:Logcat内存日志各字段含义,查看当前内存快照,跟踪记录内存分配,用adb查看内存情况时各行列的含义,捕获内存快照的3种方法,如何让程序暴漏内存泄漏的方法

    Investigating Your RAM Usage In this document Interpreting Log Messages                 内存分析日志中各消息的含 ...

  8. VS2005 检测内存泄漏的方法(转载)

    一.非MFC程序可以用以下方法检测内存泄露: 1.程序开始包含如下定义: #ifdef _DEBUG #define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __F ...

  9. JS内存泄漏 和Chrome 内存分析工具简介(摘)

    原文地址:http://web.jobbole.com/88463/ JavaScript 中 4 种常见的内存泄露陷阱   原文:Sebastián Peyrott 译文:伯乐在线专栏作者 - AR ...

随机推荐

  1. Atitit 智能云网络摄像机的前世今生与历史 优点 &#160;密码默认888888

    Atitit 智能云网络摄像机的前世今生与历史 优点  密码默认888888 用户名admin  密码aaaaaa 网络摄像机是一种结合传统摄像机与网络技术所产生的新一代摄像机,它可以将影像通过网络传 ...

  2. C#: using JsonReader avoid Deserialize Json to dynamic

    namespace Test { using Microshaoft; using Test.Models; using Newtonsoft.Json; using System; using Sy ...

  3. C# 使用Conditional特性而不是#if条件编译

    概述 #if/#endif 语句常用来基于同一份源码生成不同的编译结果,其中最常见的就是debug版和release版.但是这些工具在实际应用中并不是非常友好,因为它们容易被滥用,其代码页进而难以理解 ...

  4. [C#] 编程控制笔记本蓝牙与外部蓝牙设备通信

    一.蓝牙模块XLBT232‐D01介绍(外部设备蓝牙) 1.1.蓝牙模块简介 XLBT232-D0101蓝牙模块采用CSR BlueCore 芯片,配置6-8Mbit 的软件存储空间, 支持AT 指令 ...

  5. The Dirichlet Distribution 狄利克雷分布 (PRML 2.2.1)

    The Dirichlet Distribution 狄利克雷分布 (PRML 2.2.1) Dirichlet分布可以看做是分布之上的分布.如何理解这句话,我们可以先举个例子:假设我们有一个骰子,其 ...

  6. 学习tcl的资源

     在这里介绍一些学习tcl的资源,有问题的时候可以尝试从这些资源中获取帮助.       网站:     http://www.tcl.tk 官方站点     http://www.scriptics ...

  7. 安卓,分享到facebook的若干种方法汇总

    近期做了facebook的分享功能,遇到了很多问题,这里总结一下,供大家参考,不足之处还请大家指正. facebook分享方式: 1.通过intent调用调用本地facebook应用方式 支持单独分享 ...

  8. webrtc之视频显示模块--video_render

    在上一篇博文中,简单介绍了webrtc为我们提供了跨平台的视频采集模块,这篇博文也简单介绍下webrtc为我们提供的跨平台的视频显示模块:video_render. 该模块的源码结构如下: 如上图,我 ...

  9. BZOJ 1901 Dynamic Rankings 树董事长

    标题效果:间隔可以改变k少 我的两个天树牌主席... 隔断Count On A Tree 之后我一直认为,随着树的主席的变化是分域林木覆盖率可持久段树. .. 事实上,我是误导... 尼可持久化线段树 ...

  10. [.Net跨平台]部署DTCMS到Jexus遇到的问题及解决思路---部署

    上一篇我们环境已经准备完成,此时可以部署了,我们就以dtcms作为例子,http://bbs.dtcms.net/forum.php?mod=viewthread&tid=2420&e ...