原文:4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
筆記:塗鴉碼龍javascript譯者注:本文並無逐字逐句的翻譯,而是把我認爲重要的信息作了翻譯。若是您的英文熟練,能夠直接閱讀原文。java
本文將探索常見的客戶端 JavaScript 內存泄露,以及如何使用 Chrome 開發工具發現問題。node
內存泄露是每一個開發者最終都要面對的問題,它是許多問題的根源:反應遲緩,崩潰,高延遲,以及其餘應用問題。git
本質上,內存泄露能夠定義爲:應用程序再也不須要佔用內存的時候,因爲某些緣由,內存沒有被操做系統或可用內存池回收。編程語言管理內存的方式各不相同。只有開發者最清楚哪些內存不須要了,操做系統能夠回收。一些編程語言提供了語言特性,能夠幫助開發者作此類事情。另外一些則寄但願於開發者對內存是否須要清晰明瞭。github
JavaScript 是一種垃圾回收語言。垃圾回收語言經過週期性地檢查先前分配的內存是否可達,幫助開發者管理內存。換言之,垃圾回收語言減輕了「內存仍可用」及「內存仍可達」的問題。二者的區別是微妙而重要的:僅有開發者瞭解哪些內存在未來仍會使用,而不可達內存經過算法肯定和標記,適時被操做系統回收。算法
垃圾回收語言的內存泄露主因是不須要的引用。理解它以前,還需瞭解垃圾回收語言如何辨別內存的可達與不可達。chrome
大部分垃圾回收語言用的算法稱之爲 Mark-and-sweep 。算法由如下幾步組成:編程
現代的垃圾回收器改良了算法,可是本質是相同的:可達內存被標記,其他的被看成垃圾回收。數組
不須要的引用是指開發者明知內存引用再也不須要,卻因爲某些緣由,它仍被留在激活的 root 樹中。在 JavaScript 中,不須要的引用是保留在代碼中的變量,它再也不須要,卻指向一塊本該被釋放的內存。有些人認爲這是開發者的錯誤。瀏覽器
爲了理解 JavaScript 中最多見的內存泄露,咱們須要瞭解哪一種方式的引用容易被遺忘。
JavaScript 處理未定義變量的方式比較寬鬆:未定義的變量會在全局對象建立一個新變量。在瀏覽器中,全局對象是 window
。
1 |
function foo(arg) { |
真相是:
1 |
function foo(arg) { |
函數 foo
內部忘記使用 var
,意外建立了一個全局變量。此例泄露了一個簡單的字符串,無傷大雅,可是有更糟的狀況。
另外一種意外的全局變量可能由 this
建立:
1 |
function foo() { |
在 JavaScript 文件頭部加上
'use strict'
,能夠避免此類錯誤發生。啓用嚴格模式解析 JavaScript ,避免意外的全局變量。
全局變量注意事項
儘管咱們討論了一些意外的全局變量,可是仍有一些明確的全局變量產生的垃圾。它們被定義爲不可回收(除非定義爲空或從新分配)。尤爲當全局變量用於臨時存儲和處理大量信息時,須要多加當心。若是必須使用全局變量存儲大量數據時,確保用完之後把它設置爲 null 或者從新定義。與全局變量相關的增長內存消耗的一個主因是緩存。緩存數據是爲了重用,緩存必須有一個大小上限纔有用。高內存消耗致使緩存突破上限,由於緩存內容沒法被回收。
在 JavaScript 中使用 setInterval
很是日常。一段常見的代碼:
1 |
var someResource = getData(); |
此例說明了什麼:與節點或數據關聯的計時器再也不須要,node
對象能夠刪除,整個回調函數也不須要了。但是,計時器回調函數仍然沒被回收(計時器中止纔會被回收)。同時,someResource
若是存儲了大量的數據,也是沒法被回收的。
對於觀察者的例子,一旦它們再也不須要(或者關聯的對象變成不可達),明確地移除它們很是重要。老的 IE 6 是沒法處理循環引用的。現在,即便沒有明確移除它們,一旦觀察者對象變成不可達,大部分瀏覽器是能夠回收觀察者處理函數的。
觀察者代碼示例:
1 |
var element = document.getElementById('button'); |
對象觀察者和循環引用注意事項
老版本的 IE 是沒法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會致使內存泄露。現在,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法,已經能夠正確檢測和處理循環引用了。換言之,回收節點內存時,沒必要非要調用 removeEventListener
了。
有時,保存 DOM 節點內部數據結構頗有用。假如你想快速更新表格的幾行內容,把每一行 DOM 存成字典(JSON 鍵值對)或者數組頗有意義。此時,一樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另外一個在字典中。未來你決定刪除這些行時,須要把兩個引用都清除。
1 |
var elements = { |
此外還要考慮 DOM 樹內部或子節點的引用問題。假如你的 JavaScript 代碼中保存了表格某一個 <td>
的引用。未來決定刪除整個表格的時候,直覺認爲 GC 會回收除了已保存的 <td>
之外的其它節點。實際狀況並不是如此:此 <td>
是表格的子節點,子元素與父元素是引用關係。因爲代碼保留了 <td>
的引用,致使整個表格仍待在內存中。保存 DOM 元素引用的時候,要當心謹慎。
閉包是 JavaScript 開發的一個關鍵方面:匿名函數能夠訪問父級做用域的變量。
代碼示例:
1 |
var theThing = null; |
代碼片斷作了一件事情:每次調用 replaceThing
,theThing
獲得一個包含一個大數組和一個新閉包(someMethod
)的新對象。同時,變量 unused
是一個引用 originalThing
的閉包(先前的 replaceThing
又調用了 theThing
)。思緒混亂了嗎?最重要的事情是,閉包的做用域一旦建立,它們有一樣的父級做用域,做用域是共享的。someMethod
能夠經過 theThing
使用,someMethod
與 unused
分享閉包做用域,儘管 unused
從未使用,它引用的 originalThing
迫使它保留在內存中(防止被回收)。當這段代碼反覆運行,就會看到內存佔用不斷上升,垃圾回收器(GC)並沒有法下降內存佔用。本質上,閉包的鏈表已經建立,每個閉包做用域攜帶一個指向大數組的間接的引用,形成嚴重的內存泄露。
Meteor 的博文 解釋瞭如何修復此種問題。在
replaceThing
的最後添加originalThing = null
。
Chrome 提供了一套很棒的檢測 JavaScript 內存佔用的工具。與內存相關的兩個重要的工具:timeline
和 profiles
。
timeline 能夠檢測代碼中不須要的內存。在此截圖中,咱們能夠看到潛在的泄露對象穩定的增加,數據採集快結束時,內存佔用明顯高於採集初期,Node(節點)的總量也很高。種種跡象代表,代碼中存在 DOM 節點泄露的狀況。
Profiles 是你能夠花費大量時間關注的工具,它能夠保存快照,對比 JavaScript 代碼內存使用的不一樣快照,也能夠記錄時間分配。每一次結果包含不一樣類型的列表,與內存泄露相關的有 summary(概要) 列表和 comparison(對照) 列表。
summary(概要) 列表展現了不一樣類型對象的分配及合計大小:shallow size(特定類型的全部對象的總大小),retained size(shallow size 加上其它與此關聯的對象大小)。它還提供了一個概念,一個對象與關聯的 GC root 的距離。
對比不一樣的快照的 comparison list 能夠發現內存泄露。
實質上有兩種類型的泄露:週期性的內存增加致使的泄露,以及偶現的內存泄露。顯而易見,週期性的內存泄露很容易發現;偶現的泄露比較棘手,通常容易被忽視,偶爾發生一次可能被認爲是優化問題,週期性發生的則被認爲是必須解決的 bug。
以 Chrome 文檔中的代碼爲例:
1 |
var x = []; |
當 grow
執行的時候,開始建立 div 節點並插入到 DOM 中,而且給全局變量分配一個巨大的數組。經過以上提到的工具能夠檢測到內存穩定上升。
timeline 標籤擅長作這些。在 Chrome 中打開例子,打開 Dev Tools ,切換到 timeline,勾選 memory 並點擊記錄按鈕,而後點擊頁面上的 The Button
按鈕。過一陣中止記錄看結果:
兩種跡象顯示出現了內存泄露,圖中的 Nodes(綠線)和 JS heap(藍線)。Nodes 穩定增加,並未降低,這是個顯著的信號。
JS heap 的內存佔用也是穩定增加。因爲垃圾收集器的影響,並不那麼容易發現。圖中顯示內存佔用忽漲忽跌,實際上每一次下跌以後,JS heap 的大小都比原先大了。換言之,儘管垃圾收集器不斷的收集內存,內存仍是週期性的泄露了。
肯定存在內存泄露以後,咱們找找根源所在。
切換到 Chrome Dev Tools 的 profiles 標籤,刷新頁面,等頁面刷新完成以後,點擊 Take Heap Snapshot 保存快照做爲基準。然後再次點擊 The Button
按鈕,等數秒之後,保存第二個快照。
篩選菜單選擇 Summary,右側選擇 Objects allocated between Snapshot 1 and Snapshot 2,或者篩選菜單選擇 Comparison ,而後能夠看到一個對比列表。
此例很容易找到內存泄露,看下 (string)
的 Size Delta
Constructor,8MB,58個新對象。新對象被分配,可是沒有釋放,佔用了8MB。
若是展開 (string)
Constructor,會看到許多單獨的內存分配。選擇某一個單獨的分配,下面的 retainers 會吸引咱們的注意。
咱們已選擇的分配是數組的一部分,數組關聯到 window
對象的 x
變量。這裏展現了從巨大對象到沒法回收的 root(window
)的完整路徑。咱們已經找到了潛在的泄露以及它的出處。
咱們的例子還算簡單,只泄露了少許的 DOM 節點,利用以上提到的快照很容易發現。對於更大型的網站,Chrome 還提供了 Record Heap Allocations 功能。
回到 Chrome Dev Tools 的 profiles 標籤,點擊 Record Heap Allocations。工具運行的時候,注意頂部的藍條,表明了內存分配,每一秒有大量的內存分配。運行幾秒之後中止。
上圖中能夠看到工具的殺手鐗:選擇某一條時間線,能夠看到這個時間段的內存分配狀況。儘量選擇接近峯值的時間線,下面的列表僅顯示了三種 constructor:其一是泄露最嚴重的(string)
,下一個是關聯的 DOM 分配,最後一個是 Text
constructor(DOM 葉子節點包含的文本)。
從列表中選擇一個 HTMLDivElement
constructor,而後選擇 Allocation stack
。
如今知道元素被分配到哪裏了吧(grow
-> createSomeNodes
),仔細觀察一下圖中的時間線,發現 HTMLDivElement
constructor 調用了許屢次,意味着內存一直被佔用,沒法被 GC 回收,咱們知道了這些對象被分配的確切位置(createSomeNodes
)。回到代碼自己,探討下如何修復內存泄露吧。
在 heap allocations 的結果區域,選擇 Allocation。
這個視圖呈現了內存分配相關的功能列表,咱們馬上看到了 grow
和 createSomeNodes
。當選擇 grow
時,看看相關的 object constructor,清楚地看到 (string)
, HTMLDivElement
和 Text
泄露了。
結合以上提到的工具,能夠輕鬆找到內存泄露。
附件jpg 改 rar