概念:
兩種類型的泄露:
周期性的內存增長導致的泄露,以及偶現的內存泄露。顯而易見,周期性的內存泄露很容易發現;偶現的泄露比較棘手,一般容易被忽視,偶爾發生一次可能被認為是優化問題,周期性發生的則被認為是必須解決的 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。
五、子元素存在引用引起的內存泄漏
- 黃色是指直接被 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)
這里以cocos egret為例:打開一個界面a拍下快照,多次切換界面后回到界面a再次拍下快照。選中第二個快照,點選summary選中comparison,chrome將自動比較,此時看delta這一欄,點擊排序查看增量,結合construcor中查找你寫的對應類與增量,進行排查!
參考: