目錄
記憶體泄漏
常見的記憶體泄漏型別
1、意外的全域變數
2、被遺忘的定時器或回呼函式
3、脫離DOM的參考
4、閉包
擴展
垃圾回識訓制
參考計數法
標記清除法(常用)
記憶體泄漏
對于持續運行的服務行程(daemon),必須及時釋放不再用到的記憶體,否則,記憶體占用越來越高,輕則影響系統性能,重則導致行程崩潰, 對于不再用到的記憶體,沒有及時釋放,就叫做記憶體泄漏(memory leak),
常見的記憶體泄漏型別
1、意外的全域變數
在一個區域作用域中,未定義的變數會在全域物件創建一個新變數
function fn() {
msg = "這是一個意外的全域變數";
}
函式 fn 內部忘記使用 var ,實際上 js 會把 msg 掛載到全域物件上,意外創建一個全域變數,
類似
function fn() {
window.msg = "這是一個顯式定義的全域變數";
}
另一種意外的全域變數可能由 this 創建
function fn() {
this.msg = "該this指向window,創建了一個全域變數";
}
// fn 呼叫自己時,this 指向了全域物件(window),而不是 undefined
fn();
如何避免
在 JavaScript 檔案頭部加上 "use strict" ,使用嚴格模式避免意外的全域變數,此時上例中的this指向undefined,如果必須使用全域變數存盤大量資料時,確保用完以后把它設定為 null 或者重新定義
<script>
"use strict"
// 以下的所有代碼都處于嚴格模式
</script>
2、被遺忘的定時器或回呼函式
定時器setInterval、setTimeout代碼很常見
let data = getData();
setInterval(() => {
let el = document.getElementById('El');
if(el) {
// 處理 el 和 data
el.innerHTML = JSON.stringify(data);
}
}, 1000);
以上例子中,在 el 或者資料不再需要時(如節點移除),定時器仍然指向這些資料,所以就算 el 節點被移除后,setInterval 仍舊存活且垃圾回收器沒辦法回收,它的依賴自然也沒辦法被回收,除非終止定時器,如
let data = getData();
let timerId = setInterval(() => {
let el = document.getElementById('El');
if(el) {
// 處理 el 和 data
el.innerHTML = JSON.stringify(data);
}
}, 1000);
// 終止定時器,使得它的依賴(el、data)可被回收
clearInterval(timerId)
// setTimeout使用clearTimeout()
補充
let btn = document.getElementById('button');
function onClick(event) {
btn.innerHTML = 'text';
}
btn.addEventListener('click', onClick);
對于監聽系結,一旦它們不再需要(或者關聯的物件變成不可達),明確地移除它們非常重要,老的 IE 6 是無法處理回圈參考的,因為老版本的 IE 是無法檢測 DOM 節點與 JavaScript 代碼之間的回圈參考,會導致記憶體泄漏,
// 明確移除監聽器
btn.removeEventListener('click', onClick);
但是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收演算法(標記清除),已經可以正確檢測和處理回圈參考了,即回收節點記憶體時,不必非要呼叫 removeEventListener 了,
3、脫離DOM的參考
如果把DOM 存成字典(JSON 鍵值對)或者陣列,此時,同樣的 DOM 元素存在兩個參考:一個在 DOM 樹中,另一個在字典中,那么將來需要把兩個參考都清除
// 創建一個elements,依賴#button與#image
let elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
};
function doStuff() {
let image = document.getElementById('image');
image.src = 'http://some.url/image';
let button = document.getElementById('button');
button.click();
// ...
}
function removeButton() {
// 按鈕是 body 的后代元素
document.body.removeChild(document.getElementById('button'));
// 此時,仍舊存在一個全域的#button的參考elements字典,button元素仍舊在記憶體中,不能被GC回收,
}
// 顯式移除參考,elements = null,使依賴的DOM可被GC回收
如果代碼中保存了表格某一個 <td> 的參考,將來決定洗掉整個表格的時候,直覺認為 GC 會回收除了已保存的 <td> 以外的其它節點,實際情況并非如此:此 <td> 是表格的子節點,子元素與父元素是參考關系,由于代碼保留了 <td> 的參考,導致整個表格仍待在記憶體中,所以保存 DOM 元素參考的時候,要小心謹慎,
4、閉包
閉包的關鍵是內部函式可以訪問父級作用域的變數
let theThing = null;
let replaceThing = function () {
let originalThing = theThing;
let unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
每次呼叫 replaceThing ,theThing 得到一個包含一個大陣列和一個新閉包(someMethod)的新物件,同時,變數 unused 是一個參考 originalThing 的閉包(先前的 replaceThing 又呼叫了 theThing ),someMethod 可以通過 theThing 使用,someMethod 與 unused 分享閉包作用域,盡管 unused 從未使用,它參考的 originalThing 迫使它保留在記憶體中(防止被回收),
如何避免
在 replaceThing 的最后添加 originalThing = null ,
擴展
垃圾回識訓制
對垃圾回識訓制來說,核心思想就是如何判斷記憶體已經不再使用,常用垃圾回收演算法有下面兩種:
- 參考計數
- 標記清除(常用)
參考計數法
參考計數演算法定義“記憶體不再使用”的標準很簡單,就是看一個物件是否有指向它的參考,如果沒有其他物件指向它了,說明該物件已經不再需要了,如上我們將一些變數賦值為null,
參考計數有一個致命的問題,那就是回圈參考,如果兩個物件相互參考,盡管他們已不再使用,但是垃圾回收器不會進行回收,最終可能會導致記憶體泄漏
function cycle() {
let obj1 = {};
let obj2 = {};
// 相互參考
obj1.a = obj2;
obj2.a = obj1;
// 即使未使用到這兩個物件,兩者也都不會被GC回收
return "回圈參考!"
}
cycle();
cycle函式執行完成之后,物件obj1和obj2實際上已經不再需要了,但根據參考計數的原則,他們之間的相互參考依然存在,因此這部分記憶體不會被回收,所以現代瀏覽器不再使用這個演算法,但是IE依舊使用,
標記清除法(常用)
標記清除演算法將“不再使用的物件”定義為“無法到達的物件”,即從根部(在JS中就是全域物件)出發定時掃描記憶體中的物件,凡是能從根部到達的物件,保留,那些從根部出發無法觸及到的物件被標記為不再使用,稍后進行回收,無法觸及的物件包含了沒有參考的物件這個概念,但反之未必成立,
對于主流瀏覽器來說,只需要切斷需要回收的物件與根部的聯系,就可以正確被垃圾回收處理,最常見的記憶體泄露一般都與DOM元素系結有關
email.message = document.createElement(“div”);
displayList.appendChild(email.message);
// 稍后從displayList中清除DOM元素
displayList.removeAllChildren();
上面代碼中,div元素已經從DOM樹中清除,但是該div元素還系結在email物件的message,所以如果email物件存在,那么該div元素就會一直保存在記憶體中,
徹底清除 email = null 或 email.message = null,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/402764.html
標籤:其他
