Javascript 是Java家族中最受歡迎的成員,在該語言的開發應用過程中,其內存泄漏是一個重要的問題。中培偉業《企業級JAVA高級開發技術實戰》培訓專家劉老師在這里介紹了詳解4 種常見的 Javascript 內存泄露問題。
1: 意外的全局變量
Javascript 語言的設計目標之一是開發一種類似于 Java 但是對初學者十分友好的語言。體現 JavaScript 寬容性的一點表現在它處理未聲明變量的方式上:一個未聲明變量的引用會在全局對象中創建一個新的變量。
如果 bar 是一個應該指向 foo 函數作用域內變量的引用,但是你忘記使用 var 來聲明這個變量,這時一個全局變量就會被創建出來。在這個例子中,一個簡單的字符串泄露并不會造成很大的危害,但這無疑是錯誤的。
為了防止這種錯誤的發生,可以在你的 JavaScript 文件開頭添加 'use strict'; 語句。這個語句實際上開啟了解釋 JavaScript 代碼的嚴格模式,這種模式可以避免創建意外的全局變量。
全局變量的注意事項
盡管我們在討論那些隱蔽的全局變量,但是也有很多代碼被明確的全局變量污染的情況。按照定義來講,這些都是不會被回收的變量(除非設置 null 或者被重新賦值)。特別需要注意的是那些被用來臨時存儲和處理一些大量的信息的全局變量。如果你必須使用全局變量來存儲很多的數據,請確保在使用過后將它設置為 null 或者將它重新賦值。
常見的和全局變量相關的引發內存消耗增長的原因就是緩存。緩存存儲著可復用的數據。為了讓這種做法更高效,必須為緩存的容量規定一個上界。由于緩存不能被及時回收的緣故,緩存無限制地增長會導致很高的內存消耗。
2: 被遺漏的定時器和回調函數
JavaScript 中 setInterval 的使用十分常見。其他的庫也經常會提供觀察者和其他需要回調的功能。這些庫中的絕大部分都會關注一點,就是當它們本身的實例被銷毀之前銷毀所有指向回調的引用。
那些表示節點的對象在將來可能會被移除掉,所以將整個代碼塊放在周期處理函數中并不是必要的。然而,由于周期函數一直在運行,處理函數并不會被回收(只有周期函數停止運行之后才開始回收內存)。如果周期處理函數不能被回收,它的依賴程序也同樣無法被回收。這意味著一些資源,也許是一些相當大的數據都也無法被回收。
以前在 IE 瀏覽器的垃圾回收器上會導致一個 bug(或者說是瀏覽器設計上的問題)。舊版本的 IE 瀏覽器不會發現 DOM 節點和 JavaScript 代碼之間的循環引用。這是一種觀察者的典型情況,觀察者通常保留著一個被觀察者的引用換句話說,在 IE 瀏覽器中,每當一個觀察者被添加到一個節點上時,就會發生一次內存泄漏。這也就是開發者在節點或者空的引用被添加到觀察者中之前顯式移除處理方法的原因。
目前,現代的瀏覽器(包括 IE 和 Microsoft Edge)都使用了可以發現這些循環引用并正確的處理它們的現代化垃圾回收算法。換言之,嚴格地講,在廢棄一個節點之前調用 removeEventListener 不再是必要的操作。
像是 jQuery 這樣的框架和庫(當使用一些特定的 API 時候)都在廢棄一個結點之前移除了 listener 。它們在內部就已經處理了這些事情,并且保證不會產生內存泄露,即便程序運行在那些問題很多的瀏覽器中,比如老版本的 IE。
3: DOM 之外的引用
有些情況下將 DOM 結點存儲到數據結構中會十分有用。假設你想要快速地更新一個表格中的幾行,如果你把每一行的引用都存儲在一個字典或者數組里面會起到很大作用。如果你這么做了,程序中將會保留同一個結點的兩個引用:一個引用存在于 DOM 樹中,另一個被保留在字典中。如果在未來的某個時刻你決定要將這些行移除,則需要將所有的引用清除。
假設你在 JavaScript 代碼中保留了一個表格中特定單元格(一個 <td> 標簽)的引用。在將來你決定將這個表格從 DOM 中移除,但是仍舊保留這個單元格的引用。憑直覺,你可能會認為 GC 會回收除了這個單元格之外所有的東西,但是實際上這并不會發生:單元格是表格的一個子節點且所有子節點都保留著它們父節點的引用。換句話說,JavaScript 代碼中對單元格的引用導致整個表格被保留在內存中。所以當你想要保留 DOM 元素的引用時,要仔細的考慮清除這一點。
4: 閉包
JavaScript 開發中一個重要的內容就是閉包,它是可以獲取父級作用域的匿名函數。Meteor 的開發者發現在一種特殊情況下有可能會以一種很微妙的方式產生內存泄漏,這取決于 JavaScript 運行時的實現細節。
本質上來講,創建了一個閉包鏈表(根節點是 theThing 形式的變量),而且每個閉包作用域都持有一個對大數組的間接引用,這導致了一個巨大的內存泄露。