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內存釋放那點事

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM