Google Chrome浏览器提供了非常强大的JS调试工具,Heap Profiling便是其中一个。Heap Profiling可以记录当前的堆内存(heap)快照,并生成对象的描述文件,该描述文件给出了当时JS运行所用到的所有对象,以及这些对 ...









一、概述



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



Google Chrome 浏览器。



什么是heap



运行的时候,会有栈内存( 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 ,所以在视图里的对象都是可及的。


视图解释


列字段解释:


类名Distance --  估计是对象到根的引用层级距离


给出了当前有多少个该类的对象


对象所占内存(不包含内部引用的其它对象所占的内存) ( 单位:字节 )


对象所占总内存(包含内部引用的其它对象所占的内存) ( 单位:字节 )



下面解释一下部分类名称所代表的意思:


未知,估计是程序代码区


闭包(array) --  未知


对象类型(system) --  未知


字符串类型,有时对象里添加了新属性,属性的名称也会出现在这里


数组类型cls --  游戏大厅特有的继承类


的 window 对象


引擎的显示容器类


引擎的图片类


引擎的文本类


引擎的开关按钮类



cls 这个类名,是由于游戏大厅的继承机制里会使用“ cls ”这个引用名称,指向新建的继承类,所以凡是使用了该继承机制的类实例化出来的对象,都放在这里。例如程序中有一个类 ClassA ,继承了 Quark.Text ,则 new 出来的对象是放在 cls 里,不是放在 Quark.Text 里。



查看对象内容


@70035 ”表示的是该对象的 ID (有人会错认为是内存地址, GC 执行后,内存地址是会变的,但对象 ID 不会)。把鼠标停留在某一个对象上,会显示出该对象的内部属性和当时的值。




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



查看对象的引用关系


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




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


cls 对象的 _txtContent 变量;


context 变量;


self 变量;


0 位置;


Quark.Tween 对象的 target 变量。



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


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




(ID @70035) 其中的一条引用链是这样的:


省略 ...


                  \         |        /


                    \       |       /


                  _noticeWidget


                           |


                     _noticeC


                           |


                     _noticeV


                           |


                  _txtContent


                           ||


             Quark.Text @70035


内存快照的对比通过快照对比的功能,可以知道程序在运行期间哪些对象变更了。


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




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




Snapshot 2 ”,视图才会切换到第二次拍的快照。



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




新建了多少个对象# Deleted --  回收了多少个对象# Delta --  对象变化值,即新建的对象个数减去回收了的对象个数Size Delta --  变化的内存大小 ( 字节 )注意 Delta 字段,尤其是值大于 0 的对象。下面以 Quark.Tween 为例子,展开该对象,可看到如下图所示:



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


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


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



三、内存泄漏的排查


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


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



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


Observer )来解藕一些模块,但如果使用不当,也会带来内存泄漏的问题。


closure )和数组 Array 的对象。


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






-- 退出到分区 -- 再进入游戏 -- 再退出到分区,如此反复几次便出现游戏卡死的问题。


排查的步骤如下:


1.打开游戏;


5/10 );


3.进入后,拍下内存快照;


4.退出到刚才的分区界面;


5.再次进入同一个分区;


6.进入后,再次拍下内存快照;


2 到 6 ,直到拍下 5 组内存快照;


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 已被释放掉了。



四、结语


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


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


1.闭包上下文绑定后没有释放;


观察者模式在添加通知后,没有及时清理掉;


定时器的处理函数没有及时释放,没有调用 clearInterval 方法;


视图层有些控件重复添加,没有移除。






一、概述



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



Google Chrome 浏览器。



什么是heap



运行的时候,会有栈内存( 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 ,所以在视图里的对象都是可及的。


视图解释


列字段解释:


类名Distance --  估计是对象到根的引用层级距离


给出了当前有多少个该类的对象


对象所占内存(不包含内部引用的其它对象所占的内存) ( 单位:字节 )


对象所占总内存(包含内部引用的其它对象所占的内存) ( 单位:字节 )



下面解释一下部分类名称所代表的意思:


未知,估计是程序代码区


闭包(array) --  未知


对象类型(system) --  未知


字符串类型,有时对象里添加了新属性,属性的名称也会出现在这里


数组类型cls --  游戏大厅特有的继承类


的 window 对象


引擎的显示容器类


引擎的图片类


引擎的文本类


引擎的开关按钮类



cls 这个类名,是由于游戏大厅的继承机制里会使用“ cls ”这个引用名称,指向新建的继承类,所以凡是使用了该继承机制的类实例化出来的对象,都放在这里。例如程序中有一个类 ClassA ,继承了 Quark.Text ,则 new 出来的对象是放在 cls 里,不是放在 Quark.Text 里。



查看对象内容


@70035 ”表示的是该对象的 ID (有人会错认为是内存地址, GC 执行后,内存地址是会变的,但对象 ID 不会)。把鼠标停留在某一个对象上,会显示出该对象的内部属性和当时的值。




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



查看对象的引用关系


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




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


cls 对象的 _txtContent 变量;


context 变量;


self 变量;


0 位置;


Quark.Tween 对象的 target 变量。



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


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




(ID @70035) 其中的一条引用链是这样的:


省略 ...


                  \         |        /


                    \       |       /


                  _noticeWidget


                           |


                     _noticeC


                           |


                     _noticeV


                           |


                  _txtContent


                           ||


             Quark.Text @70035


内存快照的对比通过快照对比的功能,可以知道程序在运行期间哪些对象变更了。


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




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




Snapshot 2 ”,视图才会切换到第二次拍的快照。




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




新建了多少个对象# Deleted --  回收了多少个对象# Delta --  对象变化值,即新建的对象个数减去回收了的对象个数Size Delta --  变化的内存大小 ( 字节 )注意 Delta 字段,尤其是值大于 0 的对象。下面以 Quark.Tween 为例子,展开该对象,可看到如下图所示:




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


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


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



三、内存泄漏的排查


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


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



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


Observer )来解藕一些模块,但如果使用不当,也会带来内存泄漏的问题。


closure )和数组 Array 的对象。


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






-- 退出到分区 -- 再进入游戏 -- 再退出到分区,如此反复几次便出现游戏卡死的问题。


排查的步骤如下:


1.打开游戏;


5/10 );


3.进入后,拍下内存快照;


4.退出到刚才的分区界面;


5.再次进入同一个分区;


6.进入后,再次拍下内存快照;


2 到 6 ,直到拍下 5 组内存快照;


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 已被释放掉了。



四、结语


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


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


1.闭包上下文绑定后没有释放;


观察者模式在添加通知后,没有及时清理掉;


定时器的处理函数没有及时释放,没有调用 clearInterval 方法;


视图层有些控件重复添加,没有移除。