本文是 重溫基礎 系列文章的第二十二篇。
今日感覺:優化學習方法。 前端
系列目錄:git
本章節複習的是JS中的內存管理,這對於咱們開發很是有幫助。 github
前置知識
絕大多數的程序語言,他們的內存生命週期基本一致: 算法
對於全部的編程語言,第二部分都是明確的。而第一和第三部分在底層語言中是明確的。
但在像JavaScript
這些高級語言中,大部分都是隱含的,由於JavaScript
具備自動垃圾回收機制(Garbage collected)。
所以在作JavaScript
開發時,不須要關心內存的使用問題,所需內存分配和無用內存回收,都徹底實現自動管理。編程
像C語言這樣的高級語言通常都有底層的內存管理接口,好比malloc()
和free()
。另外一方面,JavaScript建立變量(對象,字符串等)時分配內存,而且在再也不使用它們時「自動」釋放。 後一個過程稱爲 垃圾回收。這個「自動」是混亂的根源,並讓JavaScript(和其餘高級語言)開發者感受他們能夠不關心內存管理。 這是錯誤的。
——《MDN JavaScript 內存管理》
MDN中的介紹告訴咱們,做爲JavaScript
開發者,仍是須要去了解內存管理,雖然JavaScript
已經給咱們作好自動管理。segmentfault
在作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 ++; // 使用已經定義的變量,作遞增操做
當咱們前面定義好的變量或函數(分配的內存)已經不須要使用的時候,便須要釋放掉這些內存。這也是內存管理中最難的任務,由於咱們不知道何時這些內存不使用。
很好的是,在高級語言解釋器中,已經嵌入「垃圾回收器」,用來跟蹤內存的分配和使用,以便在內存不使用時自動釋放(這並非百分百跟蹤到,只是個近似過程)。微信
就像前面提到的,「垃圾回收器」只能解決通常狀況,接下來咱們須要瞭解主要的垃圾回收算法和它們侷限性。
垃圾回收算法主要依賴於引用的概念。
即在內存管理環境中,一個對象若是有權限訪問另外一個對象,不論顯式仍是隱式,稱爲一個對象引用另外一個對象。
例如:一個JS對象具備對它原型的引用(隱式引用)和對它屬性的引用(顯式引用)。
注意:
這裏的對象,不只包含JS對象,也包含函數做用域(或全局詞法做用域)。
這個算法,把「對象是否再也不須要」定義爲:當一個對象沒有被其餘對象所引用的時候,回收該對象。這是最初級的垃圾收集算法。
var obj = { leo : { age : 18 }; };
這裏建立2個對象,一個做爲leo
的屬性被引用,另外一個被分配給變量obj
。
// 省略上面的代碼 /* 咱們將前面的 { leo : { age : 18 }; }; 稱爲「這個對象」 */ var obj2 = obj; // obj2變量是第二個對「這個對象」的引用 obj = 'pingan'; // 將「這個對象」的原始是引用obj換成obj2 var leo2 = obj2.leo; // 引用「這個對象」的leo屬性
能夠看出,如今的「這個對象」已經有2個引用,一個是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();
能夠看出,它們被調用以後,會離開函數做用域,已經沒有用了能夠被回收,然而引用計數算法考慮到它們之間相互至少引用一次,因此它們不會被回收。
實際案例:
在IE6,7中,使用引用計數方式對DOM對象進行垃圾回收,經常形成對象被循環引用致使內存泄露:
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
屬性)則會致使佔用內存永遠沒法釋放,出現內存泄露。
這個算法,將「對象是否再也不須要」定義爲:對象是否能夠得到。
標記清除算法,是假定設置一個根對象(root),在JS中是全局對象。垃圾回收器定時找全部從根開始引用的對象,而後再找這些對象引用的對象...直到找到全部能夠得到的對象和蒐集全部不能得到的對象。
它比引用計數垃圾收集更好,由於「有零引用的對象」老是不可得到的,可是相反卻不必定,參考「循環引用」。
循環引用再也不是問題:
function fun(){ var obj1 = {}, obj2 = {}; obj1.leo = obj2; // obj1引用obj2 obj2.leo = obj1; // obj2引用obj1 return 'hi pingan'; } fun();
仍是這個代碼,能夠看出,使用標記清除算法來看,函數調用以後,兩個對象沒法從全局對象獲取,所以將被回收。相同的,下面案例,一旦 obj
和其事件處理沒法從根獲取到,他們將會被垃圾回收器回收。
var obj; window.onload = function(){ obj = document.getElementById('myId'); obj.leo = obj; obj.data = new Array(100000).join(''); };
注意: 那些沒法從根對象查詢到的對象都將被清除。
在平常開發中,應該注意及時切斷須要回收對象與根的聯繫,雖然標記清除算法已經足夠強壯,就像下面代碼:
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
的閉包。
整個案例中,閉包之間共享做用域,儘管unused
可能一直沒有調用,但my_fun
可能被調用,就會致使內存沒法回收,內存增加致使泄露。
當咱們把DOM的引用保存在一個數組或Map中,即便移除了元素,但仍然有引用,致使沒法回收內存。例如:
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
仍有引用,沒法回收。
經過Chrome瀏覽器查看內存佔用:
步驟以下:
若是內存佔用基本平穩,接近水平,就說明不存在內存泄漏。
反之,就是內存泄漏了。
命令行可使用 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
字段爲準。
本部份內容到這結束
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推薦 | https://github.com/pingan8787... |
JS小冊 | js.pingan8787.com |
微信公衆號 | 前端自習課 |