本文是 重溫基礎 系列文章的第二十二篇。
本章節複習的是JS中的內存管理,這對於我們開發非常有幫助。
前置知識
對於所有的編程語言,第二部分都是明確的。而第一和第三部分在底層語言中是明確的。
JavaScript
像C語言這樣的高級語言一般都有底層的內存管理介面,比如 malloc()和free()。另一方面,JavaScript創建變數(對象,字元串等)時分配內存,並且在不再使用它們時「自動」釋放。 後一個過程稱為垃圾回收。這個「自動」是混亂的根源,並讓JavaScript(和其他高級語言)開發者感覺他們可以不關心內存管理。 這是錯誤的。 ——《MDN JavaScript 內存管理》
malloc()
free()
MDN中的介紹告訴我們,作為JavaScript開發者,還是需要去了解內存管理,雖然JavaScript已經給我們做好自動管理。
在做JavaScript開發時,我們定義變數的時候,JavaScript便為我們完成了內存分配:
var num = 100; // 為數值變數分配內存 var str = pingan; // 為字元串變數分配內存 var obj = { name : pingan }; // 為對象變數及其包含的值分配內存 var arr = [1, null, hi]; // 為數組變數及其包含的值分配內存 function fun(num){ return num + 2; }; // 為函數(可調用的對象)分配內存 // 函數表達式也能分配一個對象 someElement.addEventListener(click, function(){ someElement.style.backgroundColor = blue; }, false);
另外,通過調用函數,也會分配內存:
// 類型1. 分配對象內存 var date = new Date(); // 分配一個Date對象 var elem = document.createElement(div); // 分配一個DOM元素 // 類型2. 分配新變數或者新對象 var str1 = "pingan"; var str2 = str1.substr(0, 3); // str2 是一個新的字元串 var arr1 = ["hi", "pingan"]; var arr2 = ["hi", "leo"]; var arr3 = arr1.concat(arr2); // arr3 是一個新的數組(arr1和arr2連接的結果)
使用內存的過程實際上是對分配的內存進行讀取與寫入的操作。
讀取與寫入可能是寫入一個變數或者一個對象的屬性值,甚至傳遞函數的參數。
var num = 1; num ++; // 使用已經定義的變數,做遞增操作
當我們前面定義好的變數或函數(分配的內存)已經不需要使用的時候,便需要釋放掉這些內存。這也是內存管理中最難的任務,因為我們不知道什麼時候這些內存不使用。
就像前面提到的,「垃圾回收器」只能解決一般情況,接下來我們需要了解主要的垃圾回收演算法和它們局限性。
垃圾回收演算法主要依賴於引用的概念。
這個演算法,把「對象是否不再需要」定義為:當一個對象沒有被其他對象所引用的時候,回收該對象。這是最初級的垃圾收集演算法。
var obj = { leo : { age : 18 }; };
這裡創建2個對象,一個作為leo的屬性被引用,另一個被分配給變數obj。
leo
obj
// 省略上面的代碼 /* 我們將前面的 { leo : { age : 18 }; }; 稱為「這個對象」 */ var obj2 = obj; // obj2變數是第二個對「這個對象」的引用 obj = pingan; // 將「這個對象」的原始是引用obj換成obj2 var leo2 = obj2.leo; // 引用「這個對象」的leo屬性
可以看出,現在的「這個對象」已經有2個引用,一個是obj2,另一個是leo2。
obj2
leo2
obj2 = hi; // 將obj2變成零引用,因此,obj2可以被垃圾回收 // 但是它的屬性leo還在被leo2對象引用,所以還不能回收 leo2 = null; // 將leo變成零引用,這樣obj2和leo2都可以被垃圾回收
這個演算法有個限制:
function fun(){ var obj1 = {}, obj2 = {}; obj1.leo = obj2; // obj1引用obj2 obj2.leo = obj1; // obj2引用obj1 return hi pingan; } fun();
可以看出,它們被調用之後,會離開函數作用域,已經沒有用了可以被回收,然而引用計數演算法考慮到它們之間相互至少引用一次,所以它們不會被回收。
實際案例:
var obj; window.onload = function(){ obj = document.getElementById(myId); obj.leo = obj; obj.data = new Array(100000).join(); };
可以看出,DOM元素obj中的leo屬性引用了自己obj,造成循環引用,若該屬性(leo)沒有移除或設置為null,垃圾回收器總是且至少有一個引用,並一直佔用內存,即使從DOM樹刪除,如果這個DOM元素含大量數據(如data屬性)則會導致佔用內存永遠無法釋放,出現內存泄露。
null
data
這個演算法,將「對象是否不再需要」定義為:對象是否可以獲得。
標記清除演算法,是假定設置一個根對象(root),在JS中是全局對象。垃圾回收器定時找所有從根開始引用的對象,然後再找這些對象引用的對象...直到找到所有可以獲得的對象和搜集所有不能獲得的對象。
它比引用計數垃圾收集更好,因為「有零引用的對象」總是不可獲得的,但是相反卻不一定,參考「循環引用」。
循環引用不再是問題:
還是這個代碼,可以看出,使用標記清除演算法來看,函數調用之後,兩個對象無法從全局對象獲取,因此將被回收。相同的,下面案例,一旦 obj 和其事件處理無法從根獲取到,他們將會被垃圾回收器回收。
注意: 那些無法從根對象查詢到的對象都將被清除。
在日常開發中,應該注意及時切斷需要回收對象與根的聯繫,雖然標記清除演算法已經足夠強壯,就像下面代碼:
var obj,ele=document.getElementById(myId); obj.div = document.createElement(div); ele.appendChild(obj.div); // 刪除DOM元素 ele.removeChild(obj.div);
如果我們只是做小型項目開發,JS用的比較少的話,內存管理可以不用太在意,但是如果是大項目(SPA,伺服器或桌面應用),那就需要考慮好內存管理問題了。
在計算機科學中,內存泄漏指由於疏忽或錯誤造成程序未能釋放已經不再使用的內存。內存泄漏並非指內存在物理上的消失,而是應用程序分配某段內存後,由於設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費。 ——維基百科
其實簡單理解:一些不再使用的內存無法被釋放。
未定義的變數,會被定義到全局,當頁面關閉才會銷毀,這樣就造成內存泄露。如下:
function fun(){ name = pingan; };
var data = {}; setInterval(function(){ var render = document.getElementById(myId); if(render){ render.innderHTML = JSON.stringify(data); } }, 1000);
var str = null; var fun = function(){ var str2 = str; var unused = function(){ if(str2) console.log(is unused); }; str = { my_str = new Array(100000).join(--); my_fun = function(){ console.log(is my_fun); }; }; }; setInterval(fun, 1000);
定時器中每次調用fun,str都會獲得一個包含巨大的數組和一個對於新閉包my_fun的對象,並且unused是一個引用了str2的閉包。
fun
str
my_fun
unused
str2
var ele = { img : document.getElementById(my_img) }; function fun(){ ele.img.src = "http://www.baidu.com/1.png"; }; function foo(){ document.body.removeChild(document.getElementById(my_img)); };
即使foo方法將my_img元素移除,但fun仍有引用,無法回收。
foo
my_img
通過Chrome瀏覽器查看內存佔用:
步驟如下:
如果內存佔用基本平穩,接近水平,就說明不存在內存泄漏。
反之,就是內存泄漏了。
命令行可以使用 Node 提供的process.memoryUsage方法。
process.memoryUsage
console.log(process.memoryUsage()); // { rss: 27709440, // heapTotal: 5685248, // heapUsed: 3449392, // external: 8772 }
process.memoryUsage返回一個對象,包含了 Node 進程的內存佔用信息。該對象包含四個欄位,單位是位元組,含義如下。
rss(resident set size)
heapTotal
heapUsed
external
判斷內存泄漏,以heapUsed欄位為準。
本部分內容到這結束
推薦閱讀: