js内存回收

概念:

两种类型的泄露:

周期性的内存增长导致的泄露,以及偶现的内存泄露。显而易见,周期性的内存泄露很容易发现;偶现的泄露比较棘手,一般容易被忽视,偶尔发生一次可能被认为是优化问题,周期性发生的则被认为是必须解决的 bug。

js中堆和栈

栈:stack - 存放原始值(简单数据类型),连续的存储空间。栈空间小,读写快。

堆:heap - 存放引用值(new arry object...),散列的存储空间。堆空间大,读写慢。

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

js对象:

本地对象:(object  array  function)

宿主对象:(dom bom)

本地对象之间使用标记清除,不会造成内存泄漏。

本地和宿主对象之间使用引用计数,关联至当下cocos egret 引擎。(循环引用,闭包)

总方针:在使用完毕后切断引用链,解除事件绑定。

堆的内存释放由一特定算法的垃圾收集器进行(GC):标记清除 引用计数 复制算法

本质:当一个对象无用的时候,即程序中无变量引用这个对象时,就会从内存中释放掉这个变量。  

1、标记清除

function test(){
  var a = 10;//被标记进入环境  
}
test();//执行结束后被标记离开环境 被回收

 

2、引用计数

function test(){
  var a = {}; //a的引用次数为0
  var b = a; //a的引用次数为1
  var c = a;//a的引用次数为2
  var b = {};  //a的引用次数减1 为 1 
}

当a 为零的时候,gc会将其回收销毁。

注意:循环引用计数,相互引用将无法使用引用计数回收。

function fn(){
  var a = {};
  var b ={};
  a.obj = b;
  b.obj = a;
}
fn();
var element = document.getElementById(" ...");

var myObj = new Object();

myObj.e = element;

element.o = myObj;

这例子Dom对象element和本地对象myObj之间循环引用 

简单描述:

  三个对象 A 、B 、C

     若A的某一属性引用着B,同样C也被B的属性引用着。如果将A清除,那么B、C也被释放。

     若这里增加了C的某一属性引用B对象,如果这是清除A,那么B、C不会被释放,因为B和C之间产生了循环引用。

 var a = {};
    a.pro = { a:100 };
    a.pro.pro = { b:100 };
    a = null ; 
    //这种情况下,{a:100}和{b:100}就同时也被释放了。
            
    var obj = {};
    obj.pro = { a : 100 };
    obj.pro.pro = { b : 200 };
    var two = obj.pro.pro;
    obj = null;    
    //这种情况下 {b:200}不会被释放掉,而{a:100}被释放了。
 

 

3、内存泄漏常见的情况

一、意外的全局变量

function leaks(){
   leak ="xxx"; leak成为全局变量不会被回收  
}

说明:js中如果不用var声明变量,该变量将被视为window对象(全局对象)的属性,也就是全局变量.

function foo() {
    this.variable = "...";
}

// 没有对象调用foo, 也没有给它绑定this, 所以this是window
foo();

方案:添加"use strict" 可避免。

二、闭包引起的内存泄漏

function bindEvent(){
  var  obj =document.createElement("xx");
  obj.click = function(){
   //....
  }
}

闭包可以维持函数内的局部变量,使其得不到释放。

方案:将事件定义在外部, obj.click = this.clickFunction;  function clickFunction(){...}或者将其对象的引用删除obj.click = null;

window.onunload = function(){
        var one = document.getElementById( 'xx' );
        one.click = null;
    };

拓展:在cocos & egret中就可以遍历进行删除管理事件

三、没有清理dom元素引用

var element = {
  button: document.getElementById("button");
}
function shuff(){
  button.click();RemoveButton()
}
function RemoveButton(){ document.body.removeChild(document.getElementById("button")); }

虽然 removeChild 移除了button,但element里还保留着对button的引用,则button还保留在内存里面。

四、被遗忘的定时器或者回调

var data = {};
setInterval(function(){
   var node = document.getElementById("Node");
if(node){
node.innerHtml = JSON.stringify(data);
}
...},
1000)

如果id为Node的元素从Dom中移除,该定时器仍会存在,同时回调函数对data的引用,定时器外的data也无法释放。

方案:清除定时器。如果有引用变量同时设为null。

五、子元素存在引用引起的内存泄漏

code

  • 黄色是指直接被 js变量所引用,在内存里
  • 红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的
  • 子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除 

方案:纯粹refA = null 无效,需要refA = null ; refB =null;

var select = document.querySelector;
var treeRef = select('#tree');

var leafRef = select('#leaf');   //在COM树中leafRef是treeFre的一个子结点

select('body').removeChild(treeRef);//#tree不能被回收入,因为treeRef还在

方案:

treeRef = null;//tree还不能被回收,因为叶子结果leafRef还在
leafRef = null;//现在#tree可以被释放了

 使用chrome查看泄漏

Heap Profiling可以记录当前的堆内存(heap)快照,并生成对象的描述文件,该描述文件给出了当时JS运行所用到的所有对象,以及这些对象所占用的内存大小、引用的层级关系等等。(使用快照会自动执行一次gc

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

这里以cocos egret为例:打开一个界面a拍下快照,多次切换界面后回到界面a再次拍下快照。选中第二个快照,点选summary选中comparison,chrome将自动比较,此时看delta这一栏,点击排序查看增量,结合construcor中查找你写的对应类与增量,进行排查!

 

 

参考:

一个意想不到的Javascript内存泄漏

JavaScript 内存泄漏教程

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

谈一谈Javascript内存释放那点事

 

posted on 2017-09-09 18:07  可爱的小熊  阅读(7784)  评论(0编辑  收藏  举报

导航