Javascript程序是否會泄漏內存?答案是肯定的。我們很容易忘記刪除事件訂閱而導致保留對象。這在大多數情況下,這沒什麽大不了的,因為幾KB的內存似乎並不礙事?但是,這樣的幾kb累積多了,可能增加到幾MB甚至幾百MB,結果就會出現臭名昭著的“ Aw,snap!”。所以解決Javascript內存泄漏問題是很有必要的,但是在開始調試內存泄漏之前,我們應該首先了解下什麽是內存泄漏。
內存通常如何工作
在編程中,當您分配內存以將數據存儲在堆中時,需要再次釋放內存,然後OS才能將其用於其他用途。在諸如javascript之類的垃圾收集語言中,內存分配和釋放通常是在後台進行的。每當創建新對象時就會分配內存,而當垃圾收集器檢測到對該對象的所有引用均已消失時,則將其釋放。對象引用是允許訪問該對象的變量(指針)。Javascript的內存是按對象網絡來管理的,每個對象對應一個網絡節點。
當您認為會被垃圾回收的對象沒有被收集時,就會發生內存泄漏。
請注意,內存泄漏和正常行為之間的區別是意圖!所有類型的內存泄漏僅在您不希望對象保留在內存中的情況下才算。
內存泄漏的一些例子
全局變量
由於對象是否可訪問是決定垃圾收集的因素,因此從定義上講,所有全局對象都不會被收集。這不僅指窗口(nodejs上的全局窗口),還指javascript模塊導出的所有變量。
javascript模塊示例:
class SomeClass {…}
export const permanentInstance = new SomeClass();
永久實例永遠不會被收集,因為它是一個全局對象,因為它可以在任何地方導入。特別要注意的是,在大多數情況下,當發生大量內存泄漏時,問題一般不是單個全局實例,而是全局實例保留其他內容的後果,因為全局實例引用的所有內容也不會被垃圾回收。
觀察者模式(事件)
觀察者模式在javascript中非常流行,但是它也容易導致內存泄漏:每個事件訂閱都是潛在的內存泄漏。每個事件訂閱API都具有取消訂閱功能是有原因的:創建訂閱時,回調通過引用傳遞給事件源,並存儲在事件源中,直到調用取消訂閱或事件源本身被垃圾回收為止。
這意味著:每當您訂閱時,如果沒有匹配的取消訂閱,則隻要事件源存在,回調的閉包就會一直存在。此閉包泄漏了回調函數內部使用的,來自函數外部的所有變量
例:
const someData = “hello world”;
someButton = document.getElementById(“button”)
someButton.addEventListener(‘click’, () => console.log(someData))
隻要按鈕存在於頁麵中並且未刪除事件偵聽器,由於傳遞給click事件的箭頭函數創建的閉包操作,字符串hello world仍將保留在內存中。
如何調試內存泄漏
內存泄漏很難調試,通常您不知道要查找什麽,有時泄漏是靜態不變的,因此檢測非常困難,因為通常來說調試內存泄漏的最好辦法,是觀察隨著時間的推移內存消耗是否會持續變大。
發現泄漏
您可以在Web應用程序中四處單擊(或者有時甚至隻是等待),然後定期檢查內存並查看趨勢。所有創建大量對象的應用程序都會看到內存隨時間增加。這不一定是泄漏!垃圾回收在對CPU來說是費時的操作,因此垃圾回收器不會一直對內存進行完全掃描。您可以使用chrome的檢查器中的“性能”標簽來查看一段時間內的內存使用情況圖表。啟動監控,執行一些操作然後停止記錄
這看起來像一個典型的圖,內存增加,垃圾收集器進行一次掃描,內存減少到其大致起始位置。
問題是,如果它在執行頁麵上的操作時沒有下降到大致開始的位置,而是越來越高,這可能表明有內存泄漏的情況。
讀取內存快照
本節以 chrome 檢查器(Chrome Inspector)為例。內存快照也可以由其他調試器以不同的方式表示。
在chrome的memory標簽中,您可以選擇進行堆快照。這將創建整個內存網絡的大型json報告,並將其顯示為節點列表。
該列表按節點類型分組,展開後會看到給定類型的內存網絡中的每個節點。其中距離代表在內存網絡中您必須從活動域至少遍曆多少個節點才能到達的對象。Shallow大小是對象本身使用的內存量,保留大小( retained size)是對象避免垃圾回收所保留的內存量。
您會發現一些節點是javascript引擎內部的,例如編譯後的代碼,係統等,您無需擔心這些節點,因為您可以假設潛在的泄漏源於您的代碼而不是瀏覽器。
當您單擊一個節點時,會得到一個“retainers”列表,該列表是網絡中的一個路徑,可防止垃圾收集器收集對象(注:在對象網絡中,有路徑指向的不收集)。但是,如果您有一個高度互連的數據結構(例如帶有父引用的樹),那麽會有很多路徑,而chrome會選擇一個路徑顯示給您,它可能會向您顯示一個路徑,該路徑具有父子對象來回引用的無限遞歸,從而無法顯示導致保留對象的根節點對象。
因此,當您找到可疑對象時,您的目標是查看保留了它的對象,然後根據您不希望引用的位置來確定。為了解決這個問題,您可以將鼠標懸停在某個節點上或單擊該節點,然後在控製台中鍵入$0以獲得對內存中該節點的引用,這可以幫助您了解保留對象的對象是什麽。
調試泄漏
您不太可能找到所有最近的泄漏,尤其是小的泄漏,但是像在其他的優化中一樣,我們從最嚴重的泄漏開始,然後逐步解決,直到對內存使用感到滿意為止。
如果您直覺感覺哪些對象可能泄漏,並且這些對象是類實例,那麽您很幸運,這操作起來很簡單:轉到Chrome檢查器的“內存”選項卡,選擇“堆快照”,然後單擊“開始”,完成後在搜索欄中輸入您的類名稱。如果找到匹配項,則可以查看匹配項的數量,並將其與頁麵所在狀態所期望的實例數進行比較。例如,如果您有10個“Widget”實例,但隻需要2個widgets,那麽問題基本就出在這了。
如果您不知道要尋找什麽,一種策略是考慮應用程序中隨著時間的推移而創建和銷毀的主要類實例。像小widget,編輯器,ui組件等。然後進入和退出某些狀態,在該狀態下會重複創建這些對象,然後進行堆快照,並將內存中的實例數量與預期數量進行比較。
如果仍然無法獲得良好的結果,則另一種不涉及類實例的策略是3個快照技術:
- 將頁麵置於所需狀態,然後按F5鍵獲得幹淨狀態。頁麵加載後,進行堆快照。
- 執行一些操作(不刷新!),並使頁麵大致回到步驟1結束時的狀態。做另一個堆快照
- 嘗試盡可能更改頁麵的狀態,但請確保沒有刷新發生,因為刷新會清除內存,浪費我們的精力。頁麵狀態完全不同後,獲取第3個堆快照
現在,您可以單擊第3個快照,然後在類過濾器旁邊選擇在快照1和快照2之間分配的對象。
這將顯示快照1和2之間創建的所有對象,它們仍然存在於快照3中!這就是為什麽在步驟3中盡可能多地更改狀態如此重要:以確保收集到盡可能多的對象的原因。這樣可以發現一小部分的棘手的內存問題,可以根據具體情況決定是否這樣做。
總之,在涉及內存泄漏時,類實例是很好的著手的點,而匿名對象混雜在對象網絡中,很難發現這些對象的意外堆積(內存泄漏)。