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