當前位置:
首頁 > 知識 > 4種JavaScript的內存泄露及避免方法

4種JavaScript的內存泄露及避免方法

這篇文章裡面我們會討論客戶側javascript代碼中的常見種類的內存泄漏。也會學習如何用Chrome Development Tools來定位這些問題。繼續閱讀吧!

介紹

內存泄漏是每個開發者最終必須面對的問題。即使使用有內存管理的語言,也有內存可能會泄漏的情況。泄漏是很多問題的起因:變慢,崩潰,高延遲,甚至是一些和其他應用一起用所出現的問題。

內存泄漏是什麼?

本質上,內存泄漏可以定義為一個應用,由於某些原因不再需要的內存沒有被操作系統或者空閑內存池回收。編程語言支持多種管理內存的方式。這些方式可能會減少內存泄漏的幾率。然而,某一塊內存是否沒有用到實際上是一個不可判定的問題。換句話說,只有開發者可以弄清一塊內存是否可以被操作系統回收。某些編程語言提供了幫助開發者做這個的特性。其他一些語言期望開發者可以完全明確什麼時候一塊內存是沒被使用的。Wikipedia有關於手動和自動內存管理的兩篇不錯的文章。

Javascript中的內存管理

JavaScript是所謂的垃圾回收語言之一。垃圾回收語言,通過定期檢查哪些事先被分配的內存塊仍然可以被應用的其他部分「訪問」到,來幫助開發者管理內存。換句話說,垃圾回收語言從「哪些內存是仍然被需要的?」到「哪些內存是仍然可以被應用的其他部分訪問到的」減少了管理內存的問題。差異很微妙,但是很重要:當只有開發者知道一塊分配了的內存將來會被需要,訪問不到的內存可以在演算法上被決策並標記為系統回收內存。

非垃圾回收語言通常通過其他技術來管理內存:明確的內存管理,當一塊內存不需要時,開發者明確的告訴編譯器;還有引用計數,用計數與每個內存塊關聯(當計數到0時,被系統收回)。這些技術有他們自己的協定(和潛在的泄漏原因)。

JavaScript中的泄漏

在垃圾回收語言中,泄漏的主要原因是不必要的引用。為了理解什麼是不必要的引用,首先需要理解垃圾回收器是如何決策一塊內存是否可以被訪問到的。

「垃圾回收語言中的泄漏的主要原因是不必要的引用」。

Mark-and-sweep

大多數垃圾回收器使用一種被稱為mark-and-sweep的演算法。這個演算法包括下面的幾步:

1.垃圾回收器建立一個根節點的列表。根節點通常是代碼中一個一直在的引用對應的全局變數。在JavaScript中,對象是一個可以作為根節點的全局變數的例子。對象總是在線,所以垃圾回收器可以看重它並且它所有的子節點總是在線(即非垃圾)。

2.所有的根節點被檢查並且標記為活躍(即非垃圾)。所有子節點也同樣被遞歸檢查。每個從根節點可以到達的節點不會被認為垃圾。

3.所有沒被標記為活躍的內存塊現在可以被認為是垃圾。回收器現在可以釋放掉那塊內存並且還給操作系統。

現代垃圾回收器通過不同方法提升了這個演算法,但是本質是一樣的:可訪問到的內存塊被標記出來,剩下的被認為是垃圾。

不必要的引用,是開發者知道他/她不會再需要的,但由於某些原因存在於活躍根節點的樹上的內存塊,所對應的引用。在JavaScript的上下文中,不必要的引用是代碼中存在的不會再用到,指向一塊本來可以被釋放的內存的變數。一些人會證明這是開發者的錯誤。

所以想要理解哪些是JavaScript中最常見的泄漏,我們需要知道引用通常被忘記是通過哪些方式。

常見的JavaScript泄漏

1.意外的全局變數

JavaScript的目標是開發一種看起來像Java但足夠自由的被初學者使用的語言。JavaScript自由的其中一種方式是它可以處理沒有聲明的變數:一個未聲明的變數的引用在全局對象中創建了一個新變數。在瀏覽器的環境中,全局對象是。也就是說:

實際上是:

如果是僅在函數作用域內承載引用,並且你忘記用來聲明的變數,一個意外的全局變數就被創建了。在這個例子中,泄漏一個單一字元串不會有太大害處,但這的確是不好的。

另一種意外全局變數被創建的方式是通過this:

為了阻止這種錯誤發生,在你的Javascript文件最前面添加』use strict;』。這開啟了解析JavaScript的阻止意外全局的更嚴格的模式。

全局變數的一個注意事項:

即使我們談了不明的全局變數,仍然存在很多代碼被顯式的全局變數填充的情況。這是通過定義不可收集的情況(除非清零或重新賦值)。特別的,用來臨時存儲和處理大量信息的全局變數會引起關注。如果必須用全局變數來存儲很多數據,在處理完之後,確保對其清零或重新賦值。 一個在與全局連接上增加內存消耗常見的原因是緩存)。 緩存存儲重複被使用的數據。為此,為了有效,緩存必須有其大小的上限。飆出限制的緩存可能會因為內容不可被回收,導致高內存消耗。

2.被遺忘的計時器或回調

在JavaScript中的使用相當常見。其他庫提供觀察者和其他工具以回調。這些庫中大多數,在引用的實例變成不可訪問之後,負責讓回調的任何引用也不可訪問。在的情況下,這樣的代碼很常見:

這個例子表明了跳動的計時器可能發生什麼:計時器使得節點或數據的引用不再被需要了。代表的對象將來可能被移除,使得整個塊在間隔中的處理不必要。然而,處理函數,由於間隔仍然是活躍的,不能被回收(間隔需要被停掉才能回收)。如果間隔處理不能被回收,它的依賴也不能被回收。那意味著可能存儲著大量數據的,也不能被回收。

觀察者情況下,一旦不被需要(或相關的對象快要訪問不到)就創建明確移除他們的函數很重要。在過去,這由於特定瀏覽器(IE6)不能很好的管理循環引用(下面有更多相關信息),曾經尤為重要。現如今,一旦觀察對象變成不可訪問的,即使收聽者沒有明確的被移除,多數瀏覽器可以並會回收觀察者處理函數。然而,它保持了在對象被處理前明確的移除這些觀察者的好實踐。例如:

一條關於對象觀察者及循環引用的筆記

觀察者和循環引用曾經是JavaScript開發者的禍患。這是由於IE垃圾回收的一個(或者設計決議)出現的情況。IE的老版本不能檢測到DOM節點和JavaScript代碼間的循環引用。 這是一個通常為觀察到的保留引用(如同上面的例子)的觀察者的典型。 也就是說,每次在IE中對一個節點添加觀察者的時候,會導致泄漏。這是開發者在節點或空引用之前開始明確的移除處理函數的原因。 現在,現代瀏覽器(包括IE和MS Edge)使用可以剪裁這些循環和正確處理的現代垃圾回收演算法。換言之,在使一個節點不可訪問前,調用removeEventLister不是嚴格意義上必須的。

像Jquery一樣的框架和庫做了在處置一個節點前(當為其使用特定的API的時候)移除監聽者的工作。這被在庫內部處理,即使在像老版本IE一樣有問題的瀏覽器裡面跑,也會確保沒有泄漏產生。

3.超出DOM引用

有時存儲DOM節點到數據結構中可能有用。假設你想要迅速的更新一個表格幾行內容。存儲每個DOM行節點的引用到一個字典或數組會起作用。當這發生是,兩個對於同個DOM元素的引用被留存:一個在DOM樹中,另外一個在字典中。如果在將來的某些點你決定要移除這些行,需要讓兩個引用都不可用。

對此的額外考慮,必須處理DOM樹內的內部節點或葉子節點。假設你在JavaScript代碼中保留了一個對於特定的表格內節點(一個td標籤)的引用。在將來的某個點決定從DOM中移除這個表格,但是保留對於那個節點的引用。直觀的,會假設GC會回收除那個節點之外的每個節點。在實踐中,這不會發生的:這個單節點是那個表格的子節點,子節點保留對父節點引用。換句話說,來自JavaScript代碼的表格元素的引用會引起在內存里存整個表格。當保留DOM元素的引用的時候,仔細考慮下。

4.閉包

一個JavaScript開發的關鍵點是閉包:從父級作用域捕獲變數的匿名函數。很多開發者發現,由於JavaScript runtime的實現細節,有以一種微妙的方式泄漏的可能,這種特殊的情況:

這個代碼片段做了一件事:每次被調用的時候,theThing獲取到一個包括一個大數組和新閉包()的新對象。同時,變數unused保留了一個有(從之前的對的調用)引用的閉包。已經有點疑惑了,哈?重要的是一旦一個作用域被在同個父作用域下的閉包創建,那個作用域是共享的。這種情況下,為閉包創建的作用域被共享了。有一個對的引用。即使從來沒被用過,可以通過被使用。由於和共享了閉包作用域,即使從來沒被用過,它對的引用迫使它停留在活躍狀態(不能回收)。當這個代碼片段重複運行的時候,可以看到內存使用穩步的增長。GC運行的時候,這並不會減輕。本質上,一組關聯的閉包被創建(同變數在表單中的根節點一起),這些閉包作用域中每個帶了大數組一個非直接的引用,導致了大型的泄漏。

這是一個實現構件。一個可以處理這關係的閉包的不同實現是可以想像的,就如在這篇博客中解釋的一樣。

垃圾回收的直觀行為

即使垃圾回收很方便,他們有自己的一套權衡方法。其中一個權衡是。也就是說,GC是不可預期的。通常不能確定什麼時候回收器被執行。這意味著在一些情況下,需要比程序正在使用的更多的內存。其他情況下,短的暫停在特別敏感的應用中很明顯。即使不確定性意味著不能確定回收什麼時候執行,大多數GC實現共享在分配期間,普通的回收通行證模式。如果沒有執行分配,大多數CG停留在休息狀態。考慮下面的方案:

1.執行一組大型的分配。

2.多數元素(或所有)被標記為不可訪問(假設我們置空了一個指向不再需要的緩存的引用)。

3.沒有進一步的分配執行了。

在這個方案中,大多GC不會運行任何進一步的回收通行了。換言之,即使有可用於回收的,不可訪問的引用,回收器不會要求他了。這不是嚴格的泄漏,但是也會導致比平常更高的內存使用率。

Google在 JavaScript Memory Profiling docs, example #2.文章中,提供了一個優秀的例子。

Chrome內存分析工具概覽

Chrome提供了一系列優秀的工具來分析JavaScript代碼的內存使用。這兩幅圖域內存相關:timeline圖及profile圖。

Timeline視圖

timeline視圖在發現代碼中異常內存模式是必須的。假使在找大型泄漏,在回收之後,不與增長一樣多收縮的,周期性跳躍,是一個紅色標記。在這個截圖中可以看到泄漏的對象的穩定增長是什麼樣的。即使在最後的大型回收之後,使用的內存的總量比在開始時高。節點數量也高。這都是代碼中某處DOM節點泄漏的標誌。

Profile視圖

這是你會花大部分時間看的視圖。分析視圖允許你獲得一個快照,比較JavaScript代碼中內存使用的快照。也允許記錄一段時間的分配情況。在每個結果圖中可以看不同種類的列表,但是我們任務中,關係最大的是總結列表和比較列表。

總結列表給我們不同對象的分配及匯總大小的概覽:表面大小(一個具體類別的所有對象的總和)和保存大小(表面大小加上其他對象為這個對象留存的大小)。也給我們一個對象與其GC根節點有多遠的概念。

對比列表給我們同樣的信息,但是允許我們比較不同的快照。這個對於找泄漏十分有用。

示例:使用Chrome找內存泄漏

基本上有兩種泄漏:引起內存使用周期性的增長的泄漏,以及只發生一次並不會引進一步內存增長的泄漏。很明顯,當內存是周期性的,發現泄漏更容易。這些也是最棘手的問題:如果內存經過一段時間後增長,這類型的泄漏會最終引起瀏覽器變慢或停止執行腳本。當非周期性的泄漏在其他分配中大到足夠明顯,可以很容易的發現它們。通常情況並非如此,所以他們通常被忽視。在某種程度上,發生了一次的小泄漏可以被看作一個優化議題。然而,周期性的泄漏是bug,必須修復。

例如,我們會用Chrome文檔中的一個例子。下面貼出了全部代碼:

當grow被調用的時候,會看上創建節點,並添加到DOM。也會分配一個大數組,並添加到一個被全局變數引用的數組。這會引起可以用上面提到的內存工具發現的,穩定內存增長。

垃圾回收語言通常顯示為震蕩的內存使用模式。通常情況,如果代碼在帶分配內存的循環中的時候,是期望有這個的。我們會尋找,回收後不恢復到之前情況的,周期增長的內存。

找出內存是否在周期性的增長

時間軸視圖對此很有用。在Chrome中打開這個示例,打開開發者工具,到時間軸選項卡,選內存,點擊記錄按鈕。然後到需要測的頁面,點擊「按鈕」開始泄漏內存。等一小會停止記錄,看下結果:

這個例子每一秒會持續泄漏內存。在停止記錄之後,在函數內打個斷點,以阻止腳本迫使Chrome關閉頁面。

這圖中有兩個明顯的標誌,可以看出我們在泄漏內存。節點(綠色線)和JS堆(藍色線)的圖表。節點在穩健增長,從未減少過。這是個重要的警告標誌。

JS堆同樣也展示了內存使用的穩健增長。由於垃圾收集器的影響,這更難看到了。可以看到最初的內存增長,跟隨很厲害的減少,再然後是增長,之後是一個突刺,後續是內存的掉落。換言之,即使內存收集器成功收集了很多內存,其中一些還是被周期性的泄漏了。現在我們確定了有泄漏。讓我們找到泄漏。

取得兩張快照

為了發現泄漏,我們會來的Chrome開發者工具的profile部分。為將內存使用保持在可控的水平,在做這步之前重新載入下頁面。我們會用到函數。

重新載入頁面,在載入完成之後,就照一張堆的快照。我們會把這張快照作為基準圖來使用。之後,再次點「按鈕」。等幾秒鐘,然後拍第二張快照。在拍完快照之後,建議在腳本中打個斷點,來阻止更多內存被使用。

有兩種方法可以在兩張快照中看到內存分配。可以選擇然後從右面選快照1和快照2分配的對象,或者選而不是。在兩種情況下我們可以看到在兩張快照間被分配對象的列表。

在這種情況下很容易找到泄漏:他們很大。看下()構造函數的。58個對象,8MB。這看上去有點可疑:新對象被分配了但是沒有釋放,8MB被消耗掉了。

如果我們打開(String)構造函數,我們會注意到在小塊內存分配之間,有一些大塊的分配。這些大塊立刻引起了我們的注意。如果你選擇他們其中單獨一個,可以在retainer部分下面看到一些有趣的東西。

我們看到選中的分配是一個數組的一部分。按順序,這數組被在全局對象中的變數x引用。這給出了一個從大對象到它不能回收的根節點(window)的完整的路徑。我們發現了潛在泄漏及在哪裡引用的。

到目前為止還不錯。但是我們的例子很簡單:像這個例子中的大塊分配

內存不是規範。幸運的是我們的例子也泄漏DOM節點,是更小些的泄漏。用上面的快照很容易發現這些節點,但是在更大的站點內,事情會更複雜。Chrome的最近幾個版本提供了一個最適合我們任務的額外的工具:記錄堆分配功能。

記錄堆分配來發現泄漏

廢除你之前打的斷點,讓腳本繼續跑,回到Chrome開發者工具的Profile部分。現在點「記錄堆分配」。當工具在跑時,你將注意到頂上圖表中的藍色尖刺。這代表分配。每一秒鐘我們的代碼會進行一次大型分配。讓他跑幾秒鐘,然後停下來(別忘了打斷點來阻止Chrome吃掉更多內存)。

這幅圖中可以看到這個工具的特性:選擇時間軸的一段,看看在那個時間區間分配了什麼。我們設置選中的塊儘可能的離其中一個尖刺近。在列表中只顯示了3個構造函數:其中一個是和大型泄漏(String)有關聯的,下一個是和DOM分配有關係的,最後一個是Text構造函數(DOM包含文字的葉子節點的構造函數)。從列表裡面選擇HTMLDivElement構造函數其中一個,然後切到Allocation stack。

呀!我們現在知道那個元素在哪裡被分配的了(grow->createSomeNodes)。如果我們注意下圖表中的每個尖刺,會注意到HTMLDivElement構造器被調用了很多次。如果我們回到我們的快照對比圖,我們會注意到這個構造函數顯示了很多分配,但是沒有刪除。換言之,它在沒有GC允許回收其中的一些的情況下,穩定的分配內存。這樣,會有個我們清楚的了解的對象在哪裡分配的(createSomeNodes函數),泄漏疊加的信號。現在是回到代碼的時候了,學習下,然後堵掉泄漏。

另一個有用的特性

在堆分配結果圖裡面,我們可以選「分配」視圖而不是「總覽」視圖。

這個視圖提供了我們一個函數和與之相關的內存分配列表。我們可以立即看到grow和createSomeNodes脫穎而出。選中grow的時候,我們可以看到相關對象的構造函數被其調用。我們注意到,我們到目前為止已經知道的對象的構造函數有泄漏的(String),HTMLDivElement和Text。

這些工具的結合對查找泄漏很有幫助。用起來。在生產環境的網站跑跑不同的分析(比較理想的是非壓縮或混淆的代碼)。看看是否可以找到泄漏或留存時間長過其應有時間的對象(提示:這個更難找)。

用這個特性到Dev Tools ->Settings and Enable裡面記錄堆分配棧蹤跡。在記錄之前做這個很有必要。

結論

內存泄漏可以並確實發生在像JavaScript這樣的垃圾回收語言中。這可以被忽視一段時間,最終會肆虐開來。由於這個原因,內存分析工具對查找內存泄漏有必要。跑分析工具應該是開發流程中的一環,尤其針對中型或大型應用。開始做這個來給予你的用戶可能最好的體驗。跑起來!

作者:Sebastián Peyrott

譯者:Linda

原文:https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/

譯文:https://futu.im/posts/2017-05-25-4-types-of-js-memory-leaks/

點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 JavaScript 的精彩文章:

如何在沒有實際項目經驗的情況下找到工作
JavaScript 學者必看「new」
程序員才有的表情包,拿走不謝!
細數程序員七大恨
順豐菜鳥這麼一鬧,每天可能影響百萬消費者

TAG:JavaScript |

您可能感興趣

避免使用AtomArrayBuffers中的競爭條件
避免陷入 async/await 地獄
住宿就避免不了的 Roommate Type
Kubernetes監控方面要避免的四個常見陷阱
跑步怎樣避免winter slide?
XJar: Spring-Boot JAR 包加密運行工具,避免源碼泄露以及反編譯
InvisibleShield新型屏幕保護膜有助於避免藍光
怎樣的交易會被計入eBay defect,如何避免這些defect呢?
如何避免買到假iPhone?以及購買正版iPhone的正確方式
製作人談堡壘之夜Fortnite是如何避免Pay-To-Win
怎樣避免起一個Angelababy式的中文名
Facebook變Fakebook?小扎5小時鏖戰,本可避免!
如何避免買到假iPhone?如何購買正版iPhone
Django優化:如何避免內存泄漏
《 AI Now Report 2018 》:避免 AI 淪為惡魔,問責制度迫在眉睫
《AI Now Report 2018》:避免 AI 淪為惡魔,問責制度迫在眉睫
怎樣避免起一個Angelababy式的名字
Nat Commun:科學家揭示腫瘤躲避免疫監視的新機制
薈搜全球 Facebook避免踩到的雷區
Oculus Quest最佳開發實踐:如何避免圖形管道瓶頸