JavaScript,會在創建變量(對象,字符串等)時分配內存,並且在不再使用它們時“自動”釋放內存,這個自動釋放內存的過程稱為垃圾回收。
因為自動垃圾回收機制的存在,讓大多Javascript開發者感覺他們可以不關心內存管理,所以會在一些情況下導致內存泄漏。
內存生命周期
JS 環境中分配的內存有如下聲明周期:
- 內存分配:當我們申明變量、函數、對象的時候,系統會自動為他們分配內存
- 內存使用:即讀寫內存,也就是使用變量、函數等
- 內存回收:使用完畢,由垃圾回收機制自動回收不再使用的內存
JS 的內存分配
為了不讓程序員費心分配內存,JavaScript 在定義變量時就完成了內存分配。
var n = 123; // 給數值變量分配內存 var s = "azerty"; // 給字符串分配內存 var o = { a: 1, b: null }; // 給對象及其包含的值分配內存 // 給數組及其包含的值分配內存(就像對象一樣) var a = [1, null, "abra"]; function f(a){ return a + 2; } // 給函數(可調用的對象)分配內存 // 函數表達式也能分配一個對象 someElement.addEventListener('click', function(){ someElement.style.backgroundColor = 'blue'; }, false);
有些函數調用結果是分配對象內存:
var d = new Date(); // 分配一個 Date 對象 var e = document.createElement('div'); // 分配一個 DOM 元素
有些方法分配新變量或者新對象:
var s = "azerty"; var s2 = s.substr(0, 3); // s2 是一個新的字符串 // 因為字符串是不變量, // JavaScript 可能決定不分配內存, // 只是存儲了 [0-3] 的范圍。 var a = ["ouais ouais", "nan nan"]; var a2 = ["generation", "nan nan"]; var a3 = a.concat(a2); // 新數組有四個元素,是 a 連接 a2 的結果
JS 的內存使用
使用值的過程實際上是對分配內存進行讀取與寫入的操作。
讀取與寫入可能是寫入一個變量或者一個對象的屬性值,甚至傳遞函數的參數。
var a = 10; // 分配內存 console.log(a); // 對內存的使用
JS 的內存回收
JS 有自動垃圾回收機制,那么這個自動垃圾回收機制的原理是什么呢?
其實很簡單,就是找出那些不再繼續使用的值,然后釋放其占用的內存。
大多數內存管理的問題都在這個階段。
在這里最艱難的任務是找到不再需要使用的變量。
不再需要使用的變量也就是生命周期結束的變量,是局部變量,局部變量只在函數的執行過程中存在,
當函數運行結束,沒有其他引用(閉包),那么該變量會被標記回收。
全局變量的生命周期直至瀏覽器卸載頁面才會結束,也就是說全局變量不會被當成垃圾回收。
因為自動垃圾回收機制的存在,開發人員可以不關心也不注意內存釋放的有關問題,但對無用內存的釋放這件事是客觀存在的。
不幸的是,即使不考慮垃圾回收對性能的影響,目前最新的垃圾回收算法,也無法智能回收所有的極端情況。
一、什么是內存泄漏
程序的運行需要內存。只要程序提出要求,操作系統或運行時(runtime)就必須提供內存。對於持續運行的服務進程(daemon),必須及時釋放不再用到的內存。否則,內存占用越來越高,輕則影響系統性能,重則導致進程崩潰。
本質上講,內存泄漏就是由於疏忽或錯誤造成程序未能釋放那些不再使用的內存,照成內存的浪費。
簡單地說就是申請了一塊內存空間,使用完畢后沒有釋放掉。它的一般表現方式是程序運行時間越長,占用內存越多,最終用盡全部內存,整個系統崩潰。由程序申請的一塊內存,且沒有任何一個指針指向它,那么這塊內存就泄露了。
二、內存泄漏的識別辦法
經驗法則是,如果連續5次垃圾回收之后,內存占用一次比一次大,就有內存泄漏。
這就要求實時查看內存的占用情況。
三、在Chrome瀏覽器中,我們怎么查看內存占用情況?
- 打開開發者工具,選擇 Performance 面板
- 在頂部勾選 Memory
- 點擊左上角的 record 按鈕
- 在頁面上進行各種操作,模擬用戶的使用情況
- 一段時間后,點擊對話框的 stop 按鈕,面板上就會顯示這段時間的內存占用情況(如下圖)
我們有兩種方式來判定當前是否有內存泄漏:
- 多次快照后,比較每次快照中內存的占用情況,如果呈上升趨勢,那么可以認為存在內存泄漏
- 某次快照后,看當前內存占用的趨勢圖,如果走勢不平穩,呈上升趨勢,那么可以認為存在內存泄漏
在服務器環境中使用 Node 提供的 process.memoryUsage 方法查看內存情況
console.log(process.memoryUsage()); // { // rss: 27709440, // heapTotal: 5685248, // heapUsed: 3449392, // external: 8772 // }
process.memoryUsage返回一個對象,包含了 Node 進程的內存占用信息。
該對象包含四個字段,單位是字節,含義如下:
- rss(resident set size):所有內存占用,包括指令區和堆棧。
- heapTotal:"堆"占用的內存,包括用到的和沒用到的。
- heapUsed:用到的堆的部分。
- external: V8 引擎內部的 C++ 對象占用的內存。
判斷內存泄漏,以heapUsed字段為准。
常見的內存泄露案例:
1.意外的全局變量
function foo() { bar1 = 'some text'; // 沒有聲明變量 實際上是全局變量 => window.bar1 this.bar2 = 'some text' // 全局變量 => window.bar2 } foo();
在這個例子中,意外的創建了兩個全局變量 bar1 和 bar2
2.被遺忘的定時器和回調函數
在很多庫中, 如果使用了觀察者模式, 都會提供回調方法, 來調用一些回調函數。
要記得回收這些回調函數。舉一個 setInterval的例子:
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); // 每 5 秒調用一次
如果后續 renderer 元素被移除,整個定時器實際上沒有任何作用。
但如果你沒有回收定時器,整個定時器依然有效, 不但定時器無法被內存回收,
定時器函數中的依賴也無法回收。在這個案例中的 serverData 也無法被回收。
3.閉包
在 JS 開發中,我們會經常用到閉包,一個內部函數,有權訪問包含其的外部函數中的變量。
下面這種情況下,閉包也會造成內存泄露:
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);
這段代碼,每次調用 replaceThing 時,theThing 獲得了包含一個巨大的數組和一個對於新閉包 someMethod 的對象。
同時 unused 是一個引用了 originalThing 的閉包。
這個范例的關鍵在於,閉包之間是共享作用域的,盡管 unused 可能一直沒有被調用,但是 someMethod 可能會被調用,就會導致無法對其內存進行回收。
當這段代碼被反復執行時,內存會持續增長。
4.DOM 引用
很多時候, 我們對 Dom 的操作, 會把 Dom 的引用保存在一個數組或者 Map 中。
var elements = { image: document.getElementById('image') }; function doStuff() { elements.image.src = 'http://example.com/image_name.png'; } function removeImage() { document.body.removeChild(document.getElementById('image')); // 這個時候我們對於 #image 仍然有一個引用, Image 元素, 仍然無法被內存回收. }
上述案例中,即使我們對於 image 元素進行了移除,但是仍然有對 image 元素的引用,依然無法對齊進行內存回收。
另外需要注意的一個點是,對於一個 Dom 樹的葉子節點的引用。
舉個例子: 如果我們引用了一個表格中的td元素,一旦在 Dom 中刪除了整個表格,我們直觀的覺得內存回收應該回收除了被引用的 td 外的其他元素。
但是事實上,這個 td 元素是整個表格的一個子元素,並保留對於其父元素的引用。
這就會導致對於整個表格,都無法進行內存回收。所以我們要小心處理對於 Dom 元素的引用。
四、如何避免內存泄漏
記住一個原則:不用的東西,及時歸還。
- 減少不必要的全局變量,使用嚴格模式避免意外創建全局變量。
- 在你使用完數據后,及時解除引用(閉包中的變量,dom引用,定時器清除)。
- 組織好你的邏輯,避免死循環等造成瀏覽器卡頓,崩潰的問題。