內存分析與內存泄漏定位是筆者現代 Web 開發工程化實踐之調試技巧的一部分,主要介紹 Web 開發中須要瞭解的內存分析與內存泄露定位手段,本部分涉及的參考資料統一聲明在Web 開發界面調試資料索引。javascript
不管是分佈式計算系統、服務端應用程序仍是 iOS、Android 原生應用都會存在內存泄漏問題,Web 應用天然也不可避免地存在着相似的問題。雖然由於網頁每每都是即用即走,較少地存在某個網頁長期運行的問題,即便存在內存泄漏可能表現地也不明顯;可是在某些數據展現型的,須要長期運行的頁面上,若是不及時解決內存泄漏可能會致使網頁佔據過大地內存,不只影響頁面性能,還可能致使整個系統的崩潰。前端每週清單推薦過的 How JavaScript works 就是很是不錯地介紹 JavaScript 運行機制的系列文章,其也對內存管理與內存泄漏有過度析,本文部分圖片與示例代碼即來自此係列。前端
相似於 C 這樣的語言提供了 malloc()
與 free()
這樣的底層內存管理原子操做,開發者須要顯式手動地進行內存的申請與釋放;而 Java 這樣的語言則是提供了自動化的內存回收機制,筆者在垃圾回收算法與 JVM 垃圾回收器綜述一文中有過介紹。JavaScript 也是採用的自動化內存回收機制,不管是 Object、String 等都是由垃圾回收進程自動回收處理。自動化內存回收並不意味着咱們就能夠忽略內存管理的相關操做,反而可能會致使更不易發現的內存泄漏出現。java
筆者在 JavaScript Event Loop 機制詳解與 Vue.js 中實踐應用一文中介紹過 JavaScript 的內存模型,其主要也是由堆、棧、隊列三方面組成:node
其中隊列指的是消息隊列、棧就是函數執行棧,其基本結構以下所示:算法
而主要的用戶建立的對象就存放在堆中,這也是咱們內存分析與內存泄漏定位所須要關注的主要的區域。所謂內存,從硬件的角度來看,就是無數觸發器的組合;每一個觸發器可以存放 1 bit 位的數據,不一樣的觸發器由惟一的標識符定位,開發者能夠根據該標識符讀寫該觸發器。抽象來看,咱們能夠將內存當作比特數組,而數據就是在內存中順序排布:express
JavaScript 中開發者並不須要手動地爲對象申請內存,只須要聲明變量,JavaScript Runtime 便可以自動地分配內存:json
var n = 374; // allocates memory for a number var s = 'sessionstack'; // allocates memory for a string var o = { a: 1, b: null }; // allocates memory for an object and its contained values var a = [1, null, 'str']; // (like object) allocates memory for the // array and its contained values function f(a) { return a + 3; } // allocates a function (which is a callable object) // function expressions also allocate an object someElement.addEventListener('click', function() { someElement.style.backgroundColor = 'blue'; }, false);
某個對象的內存生命週期分爲了內存分配、內存使用與內存回收這三個步驟,當某個對象再也不被須要時,它就應該被清除回收;所謂的垃圾回收器,Garbage Collector 便是負責追蹤內存分配狀況、判斷某個被分配的內存是否有用,而且自動回收無用的內存。大部分的垃圾回收器是根據引用(Reference)來判斷某個對象是否存活,所謂的引用便是某個對象是否依賴於其餘對象,若是存在依賴關係即存在引用;譬如某個 JavaScript 對象引用了它的原型對象。最簡單的垃圾回收算法便是引用計數(Reference Counting),即清除全部零引用的對象:數組
var o1 = { o2: { x: 1 } }; // 2 objects are created. // 'o2' is referenced by 'o1' object as one of its properties. // None can be garbage-collected var o3 = o1; // the 'o3' variable is the second thing that // has a reference to the object pointed by 'o1'. o1 = 1; // now, the object that was originally in 'o1' has a // single reference, embodied by the 'o3' variable var o4 = o3.o2; // reference to 'o2' property of the object. // This object has now 2 references: one as // a property. // The other as the 'o4' variable o3 = '374'; // The object that was originally in 'o1' has now zero // references to it. // It can be garbage-collected. // However, what was its 'o2' property is still // referenced by the 'o4' variable, so it cannot be // freed. o4 = null; // what was the 'o2' property of the object originally in // 'o1' has zero references to it. // It can be garbage collected.
不過這種算法每每受制於循環引用問題,即兩個無用的對象相互引用:瀏覽器
function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 references o2 o2.p = o1; // o2 references o1. This creates a cycle. } f();
稍爲複雜的算法便是所謂的標記-清除(Mark-Sweep)算法,其根據某個對象是否可達來判斷某個對象是否可用。標記-清除算法會從某個根元素開始,譬如 window 對象開始,沿着引用樹向下遍歷,標記全部可達的對象爲可用,而且清除其餘未被標記的對象。cookie
2012 年以後,幾乎全部的主流瀏覽器都實踐了基於標記-清除算法的垃圾回收器,而且各自也進行有針對性地優化。
所謂的內存泄漏,便是指某個對象被無心間添加了某條引用,致使雖然實際上並不須要了,但仍是能一直被遍歷可達,以至其內存始終沒法回收。本部分咱們簡要討論下 JavaScript 中常見的內存泄漏情境與處理方法。在新版本的 Chrome 中咱們可使用 Performance Monitor 來動態監測網頁性能的變化:
上圖中各項指標的含義爲:
當發現某個時間點可能存在內存泄漏時,咱們可使用 Memory 標籤頁將此時的堆分配狀況打印下來:
JavaScript 會將全部的爲聲明的變量當作全局變量進行處理,即將其掛載到 global 對象上;瀏覽器中這裏的 global 對象就是 window:
function foo(arg) { bar = "some text"; } // 等價於 function foo(arg) { window.bar = "some text"; }
另外一種常見的建立全局變量的方式就是誤用 this
指針:
function foo() { this.var1 = "potential accidental global"; } // Foo called on its own, this points to the global object (window) // rather than being undefined. foo();
一旦某個變量被掛載到了 window 對象,就意味着它永遠是可達的。爲了不這種狀況,咱們應該儘量地添加 use strict
或者進行模塊化編碼(參考 JavaScript 模塊演化簡史)。咱們也能夠擴展相似於下文的掃描函數,來檢測出 window 對象的非原生屬性,並加以判斷:
function scan(o) { Object.keys(o).forEach(function(key) { var val = o[key]; // Stop if object was created in another window if ( typeof val !== "string" && typeof val !== "number" && typeof val !== "boolean" && !(val instanceof Object) ) { debugger; console.log(key); } // Traverse the nested object hierarchy }); }
咱們常常會使用 setInterval
來執行定時任務,不少的框架也提供了基於回調的異步執行機制;這可能會致使回調中聲明瞭對於某個變量的依賴,譬如:
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //This will be executed every ~5 seconds.
定時器保有對於 serverData 變量的引用,若是咱們不手動清除定時器話,那麼該變量也就會一直可達,不被回收。而這裏的 serverData 也是閉包形式被引入到 setInterval 的回調做用域中;閉包也是常見的可能致使內存泄漏的元兇之一:
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // a reference to 'originalThing' console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
上述代碼中 replaceThing 會按期執行,而且建立大的數組與 someMethod 閉包賦值給 theThing。someMethod 做用域是與 unused 共享的,unused 又有一個指向 originalThing 的引用。儘管 unused 並未被實際使用,theThing 的 someMethod 方法卻有可能會被外部使用,也就致使了 unused 始終處於可達狀態。unused 又會反向依賴於 theThing,最終致使大數組始終沒法被清除。
有時候咱們可能會將 DOM 元素存放到數據結構中,譬如當咱們須要頻繁更新某個數據列表時,可能會將用到的數據列表存放在 JavaScript 數組中;這也就致使了每一個 DOM 元素存在了兩個引用,分別在 DOM 樹與 JavaScript 數組中:
var elements = { button: document.getElementById('button'), image: document.getElementById('image') }; function doStuff() { elements.image.src = 'http://example.com/image_name.png'; } function removeImage() { // The image is a direct child of the body element. document.body.removeChild(document.getElementById('image')); // At this point, we still have a reference to #button in the //global elements object. In other words, the button element is //still in memory and cannot be collected by the GC. }
此時咱們就須要將 DOM 樹與 JavaScript 數組中的引用皆刪除,才能真實地清除該對象。相似的,在老版本的瀏覽器中,若是咱們清除某個 DOM 元素,咱們須要首先移除其監聽器,不然瀏覽器並不會自動地幫咱們清除該監聽器,或者回收該監聽器引用的對象:
var element = document.getElementById('launch-button'); var counter = 0; function onClick(event) { counter++; element.innerHtml = 'text ' + counter; } element.addEventListener('click', onClick); // Do stuff element.removeEventListener('click', onClick); element.parentNode.removeChild(element); // Now when element goes out of scope, // both element and onClick will be collected even in old browsers // that don't handle cycles well.
現代瀏覽器使用的現代垃圾回收器則會幫咱們自動地檢測這種循環依賴,而且予以清除;jQuery 等第三方庫也會在清除元素以前首先移除其監聽事件。
iframe 是常見的界面共享方式,不過若是咱們在父界面或者子界面中添加了對於父界面某對象的引用,譬如:
// 子頁面內 window.top.innerObject = someInsideObject window.top.document.addEventLister(‘click’, function() { … }); // 外部頁面 innerObject = iframeEl.contentWindow.someInsideObject
就有可能致使 iframe 卸載(移除元素)以後仍然有部分對象保留下來,咱們能夠在移除 iframe 以前執行強制的頁面重載:
<a href="#">Remove</a> <iframe src="url" /> $('a').click(function(){ $('iframe')[0].contentWindow.location.reload(); // 線上環境實測重置 src 效果會更好 // $('iframe')[0].src = "javascript:false"; setTimeout(function(){ $('iframe').remove(); }, 1000); });
或者手動地執行頁面清除操做:
window.onbeforeunload = function(){ $(document).unbind().die(); //remove listeners on document $(document).find('*').unbind().die(); //remove listeners on all nodes //clean up cookies /remove items from localStorage }
現代瀏覽器中咱們常用 Web Worker 來運行後臺任務,不過有時候若是咱們過於頻繁且不加容錯地在主線程與工做線程之間傳遞數據,可能會致使內存泄漏:
function send() { setInterval(function() { const data = { array1: get100Arrays(), array2: get500Arrays() }; let json = JSON.stringify( data ); let arbfr = str2ab (json); worker.postMessage(arbfr, [arbfr]); }, 10); } function str2ab(str) { var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char var bufView = new Uint16Array(buf); for (var i=0, strLen=str.length; i<strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; }
在實際的代碼中咱們應該檢測 Transferable Objects 是否正常工做:
let ab = new ArrayBuffer(1); try { worker.postMessage(ab, [ab]); if (ab.byteLength) { console.log('TRANSFERABLE OBJECTS are not supported in your browser!'); } else { console.log('USING TRANSFERABLE OBJECTS'); } } catch(e) { console.log('TRANSFERABLE OBJECTS are not supported in your browser!'); }