JS內(nèi)存回收
JS 有自動垃圾回收機制,就是找出那些不再繼續(xù)使用的值,然后釋放其占用的內(nèi)存。
垃圾回收算法:
- 引用計數(shù)垃圾收集
- 標(biāo)記清楚算法
內(nèi)存泄露
如果連續(xù)五次垃圾回收之后,內(nèi)存占用一次比一次大,就有內(nèi)存泄漏。
在 Chrome 瀏覽器中,我們可以這樣查看內(nèi)存占用情況
- 打開頁面,右鍵選擇檢查,打開開發(fā)者工具
- 在頂部勾選 Memory
- 模擬用戶操作,并進行多次內(nèi)存快照
判定當(dāng)前是否有內(nèi)存泄漏:
- 多次快照后,比較每次快照中內(nèi)存的占用情況,如果呈上升趨勢,那么可以認(rèn)為存在內(nèi)存泄漏
- 某次快照后,看當(dāng)前內(nèi)存占用的趨勢圖,如果走勢不平穩(wěn),呈上升趨勢,那么可以認(rèn)為存在內(nèi)存泄漏
在 Node環(huán)境 查看內(nèi)存情況
console.log(process.memoryUsage());
// {
// rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772
// }
該對象包含四個字段,單位是字節(jié),含義如下:
- rss(resident set size):所有內(nèi)存占用,包括指令區(qū)和堆棧。
- heapTotal:"堆"占用的內(nèi)存,包括用到的和沒用到的。
- heapUsed:用到的堆的部分。
- external: V8 引擎內(nèi)部的 C++ 對象占用的內(nèi)存。
常見內(nèi)存泄露:
1、非必要的全局變量。如下代碼,就是創(chuàng)建了兩個全局變量name1,name2
function foo() {
name1 = 'xyj'; // 沒有聲明變量 實際上是全局變量 => window.name1
this.name2 = 'why' // 全局變量 => window.name2
}
foo();
2、被遺忘的定時器和回調(diào)函數(shù)
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); // 每 5 秒調(diào)用一次
如果后續(xù) renderer 元素被移除,整個定時器實際上沒有任何作用。 但如果你沒有回收定時器,整個定時器依然有效, 不但定時器無法被內(nèi)存回收, 定時器函數(shù)中的依賴也無法回收。在這個案例中的 serverData 也無法被回收。
3、閉包
在 JS 開發(fā)中,我們會經(jīng)常用到閉包,一個內(nèi)部函數(shù),有權(quán)訪問包含其的外部函數(shù)中的變量。 下面這種情況下,閉包也會造成內(nèi)存泄露:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // 對于 'originalThing'的引用
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
這段代碼,每次調(diào)用 replaceThing 時,theThing 獲得了包含一個巨大的數(shù)組和一個對于新閉包 someMethod 的對象。 同時 unused 是一個引用了 originalThing 的閉包。
這個范例的關(guān)鍵在于,閉包之間是共享作用域的,盡管 unused 可能一直沒有被調(diào)用,但是 someMethod 可能會被調(diào)用,就會導(dǎo)致無法對其內(nèi)存進行回收。 當(dāng)這段代碼被反復(fù)執(zhí)行時,內(nèi)存會持續(xù)增長。
function makeAdder(count) {
return function (num) {
return count + num
}
}
var add5 = makeAdder(5)
console.log(add5(6))
這段代碼中makeAdder函數(shù)執(zhí)行完畢,正常情況下我們的AO對象會被釋放; 但是因為在0xb00的函數(shù)中有作用域引用指向了這個AO對象,所以它不會被釋放掉;
解決方案 add5 = null
4、DOM 引用
很多時候, 我們對 Dom 的操作, 會把 Dom 的引用保存在一個數(shù)組或者 Map 中。
var elements = {
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/xxx.png';
}
function removeImage() {
document.body.removeChild(document.getElementById('image'));
// 這個時候我們對于 #image 仍然有一個引用, Image 元素, 仍然無法被內(nèi)存回收.
}
復(fù)制代碼
上述案例中,即使我們對于 image 元素進行了移除,但是仍然有對 image 元素的引用,依然無法對齊進行內(nèi)存回收。
另外需要注意的一個點是,對于一個 Dom 樹的葉子節(jié)點的引用。 舉個例子: 如果我們引用了一個表格中的td元素,一旦在 Dom 中刪除了整個表格,我們直觀的覺得內(nèi)存回收應(yīng)該回收除了被引用的 td 外的其他元素。 但是事實上,這個 td 元素是整個表格的一個子元素,并保留對于其父元素的引用。 這就會導(dǎo)致對于整個表格,都無法進行內(nèi)存回收。所以我們要小心處理對于 Dom 元素的引用。