內存的生命週期:
- 分配你所需要的內存:
由於字符串、對象等沒有固定的大小,js程序在每次創建字符串、對象的時候,程序都會分配內存來存儲那個實體。
- 使用分配到的內存做點什麼。
- 不需要時將其釋放回歸:
在不需要字符串、對象的時候,需要釋放其所佔用的內存,否則將會消耗完系統中所有可用的內存,造成系統崩潰,這就是垃圾回收機制所存在的意義。
所謂的內存洩漏指的是:由於疏忽或錯誤造成程序未能釋放那些已經不再使用的內存,造成內存的浪費。
垃圾回收機制:
在C和C++之類的語言中,需要手動來管理內存的,這也是造成許多不必要問題的根源。幸運的是,在編寫js的過程中,內存的分配以及內存的回收完全實現了自動管理,我們不用操心這種事情。
垃圾收集機制的原理:
垃圾收集器會按照固定的時間間隔,週期性的找出不再繼續使用的變量,然後釋放其佔用的內存。
什麼叫不再繼續使用的變量?
不再使用的變量也就是生命週期結束的變量,是局部變量,局部變量只在函數的執行過程中存在,當函數運行結束,沒有其他引用(閉包),那麼該變量會被標記回收。
全局變量的生命週期直至瀏覽器卸載頁面才會結束,也就是說全局變量不會被當成垃圾回收。
標記清除:當前採用的垃圾收集策略
工作原理:
當變量進入環境時(例如在函數中聲明一個變量),將這個變量標記為“進入環境”,當變量離開環境時,則將其標記為“離開環境”。標記“離開環境”的就回收內存。
工作流程:
- 垃圾收集器會在運行的時候會給存儲在內存中的所有變量都加上標記。
- 去掉環境中的變量以及被環境中的變量引用的變量的標記。
- 那些還存在標記的變量被視為準備刪除的變量。
- 最後垃圾收集器會執行最後一步內存清除的工作,銷燬那些帶標記的值並回收它們所佔用的內存空間。
到2008年為止,IE、Chorme、Fireofx、Safari、Opera 都使用標記清除式的垃圾收集策略,只不過垃圾收集的時間間隔互有不同。
引用計數略:被廢棄的垃圾收集策略
循環引用:跟蹤記錄每個值被引用的技術
在老版本的瀏覽器中(對,又是IE),IE9以下BOM和DOM對象就是使用C++以COM對象的形式實現的。
COM的垃圾收集機制採用的就是引用計數策略,這種機制在出現循環引用的時候永遠都釋放不掉內存。
var element = document.getElementById('something');
var myObject = new Object();
myObject.element = element; // element屬性指向dom
element.someThing = myObject; // someThing回指myObject 出現循環引用(兩個對象一直互相包含 一直存在計數)。
解決方式是,當我們不使用它們的時候,手動切斷鏈接:
myObject.element = null;
element.someThing = null;
淘汰:
IE9把BOM和DOM對象轉為了真正的js對象,避免了使用這種垃圾收集策略,消除了IE9以下常見的內存洩漏的主要原因。
IE7以下有一個聲明狼藉的性能問題,大家瞭解一下:
- 256個變量,4096個對象(或數組)字面或者64KB的字符串,達到任何一個臨界值會觸發垃圾收集器運行。
- 如果一個js腳本的生命週期一直保有那麼多變量,垃圾收集器會一直頻繁的運行,引發嚴重的性能問題。
IE7已修復這個問題。
哪些情況會引起內存洩漏?
雖然有垃圾回收機制,但我們在編寫代碼的時候,有些情況還是會造成內存洩漏,瞭解這些情況,並在編寫程序的時候,注意避免,我們的程序會更具健壯性。
意外的全局變量:
上文我們提到了全局變量不會被當成垃圾回收,我們在編碼中有時會出現下面這種情況:
function foo () {
this.bar2 = '默認綁定this指向全局' // 全局變量=> window.bar2
bar = '全局變量'; // 沒有聲明變量 實際上是全局變量=>window.bar
}
foo();
當我們使用默認綁定,this會指向全局,this.something也會創建一個全局變量,這一點可能很多人沒有注意到。
解決方法:在函數內使用嚴格模式or細心一點
function foo() {
"use strict";
this.bar2 = "嚴格模式下this指向undefined";
bar = "報錯";
}
foo();
當然我們也可以手動釋放全局變量的內存 :
window.bar = undefined
delete window.bar2
被遺忘的定時器和回調函數
當不需要setInterval或者setTimeout時,定時器沒有被clear,定時器的回調函數以及內部依賴的變量都不能被回收,造成內存洩漏。
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someResource));
// 定時器也沒有清除
}
// node、someResource 存儲了大量數據 無法回收
}, 1000);
解決方法: 在定時器完成工作的時候,手動清除定時器。
閉包:
閉包可以維持函數內局部變量,使其得不到釋放,造成內存洩漏。
function bindEvent() {
var obj = document.createElement("XXX");
var unused = function () {
console.log(obj,'閉包內引用obj obj不會被釋放');
};
// obj = null;
}
解決方法:手動解除引用,obj = null。
循環引用問題
就是IE9以下的循環引用問題,上文講過了。
沒有清理DOM元素引用:
var refA = document.getElementById('refA');
document.body.removeChild(refA); // dom刪除了
console.log(refA, "refA"); // 但是還存在引用 能console出整個div 沒有被回收
不信的話,可以看下這個dom點擊預覽。
解決辦法:refA = null;
console保存大量數據在內存中。
過多的console,比如定時器的console會導致瀏覽器卡死。
解決:合理利用console,線上項目儘量少的使用console,當然如果你要發招聘除外。
如何避免內存洩漏:
記住一個原則:不用的東西,及時歸還,畢竟你是'借的'嘛。
- 減少不必要的全局變量,使用嚴格模式避免意外創建全局變量。
- 在你使用完數據後,及時解除引用(閉包中的變量,dom引用,定時器清除)。
- 組織好你的邏輯,避免死循環等造成瀏覽器卡頓,崩潰的問題。
關於內存洩漏:
- 即使是1byte的內存,也叫內存洩漏,並不一定是導致瀏覽器崩潰、卡頓才能叫做內存洩漏。
- 一般是堆區內存洩漏,棧區不會洩漏。
基本類型的值存在內存中,被保存在棧內存中,引用類型的值是對象,保存在堆內存中。所以對象、數組之類的,才會發生內存洩漏。
- 使用chrome監控內存洩漏,可以看一下這篇文章
閱讀更多 前端小白說 的文章