Javascript有沒有記憶體外洩?如果有,如何避免?鑑於最近有好幾個人問到我類似的問題,看來大家對這部分內容還沒有系統的研究過,因此,打算在這裡把個人幾年前整理的一些資料和大家分享一下。
首先,可以肯定的說,javascript的一些寫法會造成記憶體外洩的,至少在IE6下是如此。因此,在IE6遲遲不肯退休的今天,我們還是有必要了解相關的知識(雖然大部分情況下,js造成的這點內存洩漏不是致使電腦運行變慢的主要原因)。相關的研究主要集中在05-07這幾年,本文並沒有什麼新的觀點,如果當年有研究過的朋友,可以直接忽略。
身為前端開發人員,了解這些問題的時候,需要知其然也知其所以然,因此,在介紹js記憶體外洩前,我們先從為什麼會有記憶體外洩談起。
說記憶體洩露,就不得不談到記憶體分配的方式。記憶體分配有三種方式,分別是:
一、靜態分配( Static Allocation ):靜態變數和全域變數的分配形式。如果把房間看做一個程序,我們可以把靜態分配的記憶體當成房間裡的耐用家具。通常,它們無需釋放和回收,因為沒人會天天把大衣櫃當作垃圾丟到窗外。
二、自動分配( Automatic Allocation ):在堆疊中為局部變數分配記憶體的方法。堆疊中的記憶體可以隨著程式碼區塊退出時的出棧操作自動釋放。
這類似於到房間中辦事的人,事情一旦完成,就會自己離開,而他們所佔用的空間,也隨著這些人的離開而自動釋放了。
三、動態分配( Dynamic Allocation ):在堆中動態分配記憶體空間以儲存資料的方式。也就是程式運行時用malloc或new申請的內存,我們需要自己用free或delete釋放。動態記憶體的生存期由程式設計師自己決定。一旦忘記釋放,勢必造成記憶體外洩。這種情況下,堆中的內存塊好像我們日常使用的餐巾紙,用過了就得扔到垃圾箱裡,否則屋內就會滿地狼藉。因此,懶人們做夢都想有一台家用機器人跟在身邊打掃。在軟體開發中,如果你懶得釋放內存,那麼你也需要一台類似的機器人——這其實就是一個由特定演算法實現的垃圾收集器。而正是垃圾收集機製本身的一些缺陷,導致了javascript記憶體外洩。
幾年前看過一篇叫《垃圾回收趣史》的文章,裡面對垃圾回收機製做了深入淺出的說明。
就像機械增壓這種很多豪車作為賣點的技術,其實上個世紀10年代賓士就在使用了一樣,垃圾回收技術誕生也有很長的時間了。 1960 年前後誕生於MIT 的Lisp 語言是第一個高度依賴動態記憶體分配技術的語言,Lisp 中幾乎所有資料都以「表」的形式出現,而「表」所佔用的空間則是在堆中動態分配得到的。 Lisp 語言先天就具有的動態記憶體管理特性要求Lisp 語言的設計者必須解決堆中每一個記憶體區塊的自動釋放問題(否則, Lisp 程式設計師必然被程式中不計其數的free 或delete 語句淹沒),這直接導致了垃圾收集技術的誕生和發展。
而三種最基本的垃圾回收演算法,也在那時一起出現了。下面我們一個一個了解:
引用計數(Reference Counting)演算法:這個可能是最早想到的方法。圖像點說,引用計數可以這麼理解,房子裡放了很多白紙,這些紙就好比是內存。使用內存,就好比在這些紙上寫字。記憶體可以隨便使用,但是,有個條件,任何使用一張紙的人,必須在紙的一角寫上計數1,如果2個人同時使用一張紙,那麼計數就變成2,以此類推。當一個人使用完某張紙的時候,必須把角上的計數減1,這樣,一旦當計數變為0,就滿足了垃圾回收條件,等在一旁的機器人會立即把這張紙扔進垃圾箱。基於引用計數器的垃圾收集器運作較快,不會長時間中斷程式執行,適宜地必須即時執行的程式。但引用計數器增加了程式執行的開銷;同時,還有個最大的問題,這個演算法有一個缺陷,就是一旦產生循環引用,記憶體就會被洩漏。舉個例子,我們new了2個物件a和b,這時,a和b的數數都是1,然後,我們把a的一個屬性指向b,b的一個屬性指向a,此時,由於引用的關係,a和b的計數都變成了2,當程式運行結束時,退出作用域,程式自動把a的計數減1,由於最後a的計數仍然為1,因此,a不會被釋放,同樣,b最後的計數也是1,b也不會被釋放,記憶體就這麼洩漏了!