理解並解決IE的內存泄漏方式


Web開發的發展

    在過去一些的時候,Web開發人員並沒有太多的去關注內存泄露問題。那時的頁面間聯繫大都比較簡單,並主要使用不同的連接地址在同一

個站點中導航,這樣的設計方式是非常有利於瀏覽器釋放資源的。即使Web頁面運行中真的出現了資源泄漏,那它的影響也是非常有限而且常常

是不會被人在意的。

    今天人們對Web應用有了高更的要求。一個頁面很可能數小時不會發生URL跳轉,並同時通過Web服務動態的更新頁面內容。複雜的事件關聯

設計、基於對象的JScript和DHTML技術的廣泛採用,使得代碼的能力達到了其承受的極限。在這樣的情況和改變下,弄清楚內存泄露方式變得

非常的急迫,特別是過去這些問題都被傳統的頁面導航方法給屏蔽了。

    還算好的事情是,當你明確了希望尋找什麼時,內存泄露方式是比較容易被確定的。大多數你能遇到的泄露問題我們都已經知道,你只需

要少量額外的工作就會給你帶來好處。雖然在一些頁面中少量的小泄漏問題仍會發生,但是主要的問題還是很容易解決的。

泄露方式

    在接下來的內容中,我們會討論內存泄露方式,併爲每種方式給出示例。其中一個重要的示例是JScript中的Closure技術,另一個示例是

在事件執行中使用Closures。當你熟悉本示例後,你就能找出並修改你已有的大多數內存泄漏問題,但是其它Closure相關的問題可能又會被忽

視。

現在讓我們來看看這些個方式都有什麼:

1、循環引用(Circular References) — IE瀏覽器的COM組件產生的對象實例和網頁腳本引擎產生的對象實例相互引用,就會造成內存泄漏。

這也是Web頁面中我們遇到的最常見和主要的泄漏方式;

2、內部函數引用(Closures) — Closures可以看成是目前引起大量問題的循環應用的一種特殊形式。由於依賴指定的關鍵字和語法結構,

Closures調用是比較容易被我們發現的;

3、頁面交叉泄漏(Cross-Page Leaks) — 頁面交叉泄漏其實是一種較小的泄漏,它通常在你瀏覽過程中,由於內部對象薄計引起。下面我們

會討論DOM插入順序的問題,在那個示例中你會發現只需要改動少量的代碼,我們就可以避免對象薄計對對象構建帶來的影響;

4、貌似泄漏(Pseudo-Leaks) — 這個不是真正的意義上的泄漏,不過如果你不瞭解它,你可能會在你的可用內存資源變得越來越少的時候極

度鬱悶。爲了演示這個問題,我們將通過重寫Script元素中的內容來引發大量內存的"泄漏"。

循環引用

    循環引用基本上是所有泄漏的始作俑者。通常情況下,腳本引擎通過垃圾收集器(GC)來處理循環引用,但是某些未知因數可能會妨礙從其

環境中釋放資源。對於IE來說,某些DOM對象實例的狀態是腳本無法得知的。下面是它們的基本原則:

   
    Figure 1: 基本的循環引用模型

    本模型中引起的泄漏問題基於COM的引用計數。腳本引擎對象會維持對DOM對象的引用,並在清理和釋放DOM對象指針前等待所有引用的移除

。在我們的示例中,我們的腳本引擎對象上有兩個引用:腳本引擎作用域和DOM對象的expando屬性。當終止腳本引擎時第一個引用會釋放,DOM

對象引用由於在等待腳本擎的釋放而並不會被釋放。你可能會認爲檢測並修復假設的這類問題會非常的容易,但事實上這樣基本的的示例只是

冰山一角。你可能會在30個對象鏈的末尾發生循環引用,這樣的問題排查起來將會是一場噩夢。

    如果你仍不清楚這種泄漏方式在HTML代碼裏到底怎樣,你可以通過一個全局腳本變量和一個DOM對象來引發並展現它。

   提示:您可以先修改部分代碼再運行

你可以使用直接賦null值得方式來破壞該泄漏情形。在頁面文檔卸載前賦null值,將會讓腳本引擎知道對象間的引用鏈沒

有了。現在它將能正常的清理引用並釋放DOM對象。在這個示例中,作爲Web開發員的你因該更多的瞭解了對象間的關係。

    作爲一個基本的情形,循環引用可能還有更多不同的複雜表現。對基於對象的JScript,一個通常用法是通過封裝JScript對象來擴充DOM對

象。在構建過程中,你常常會把DOM對象的引用放入JScript對象中,同時在DOM對象中也存放上對新近創建的JScript對象的引用。你的這種應

用模式將非常便於兩個對象之間的相互訪問。這是一個非常直接的循環引用問題,但是由於使用不用的語法形式可能並不會讓你在意。要破環

這種使用情景可能變得更加複雜,當然你同樣可以使用簡單的示例以便於清楚的討論。

   提示:您可以先修改部分代碼再運行

更復雜的辦法還有記錄所有需要解除引用的對象和屬性,然後在Web文檔卸載的時候統一清理,但大多數時候你可能會再造

成額外的泄漏情形,而並沒有解決你的問題。

閉包函數(Closures)
    由於閉包函數會使程序員在不知不覺中創建出循環引用,所以它對資源泄漏常常有着不可推卸的責任。而在閉包函數自己被釋放前,我們很難判斷父函數的參數以及它的局部變量是否能被釋放。實際上閉包函數的使用已經很普通,以致人們頻繁的遇到這類問題時我們卻束手無策。在詳細瞭解了閉包背後的問題和一些特殊的閉包泄漏示例後,我們將結合循環引用的圖示找到閉包的所在,並找出這些不受歡迎的引用來至何處。


Figure 2. 閉包函數引起的循環引用

    普通的循環引用,是兩個不可探知的對象相互引用造成的,但是閉包卻不同。代替直接造成引用,閉包函數則取而代之從其父函數作用域中引入信息。通常,函數的局部變量和參數只能在該被調函數自身的生命週期裏使用。當存在閉包函數後,這些變量和參數的引用會和閉包函數一起存在,但由於閉包函數可以超越其父函數的生命週期而存在,所以父函數中的局部變量和參數也仍然能被訪問。在下面的示例中,參數1將在函數調用終止時正常被釋放。當我們加入了一個閉包函數後,一個額外的引用產生,並且這個引用在閉包函數釋放前都不會被釋放。如果你碰巧將閉包函數放入了事件之中,那麼你不得不手動從那個事件中將其移出。如果你把閉包函數作爲了一個expando屬性,那麼你也需要通過置null將其清除。

    同時閉包會在每次調用中創建,也就是說當你調用包含閉包的函數兩次,你將得到兩個獨立的閉包,而且每個閉包都分別擁有對參數的引用。由於這些顯而易見的因素,閉包確實非常用以帶來泄漏。下面的示例將展示使用閉包的主要泄漏因素:

   提示:您可以先修改部分代碼再運行

如果你對怎麼避免這類泄漏感到疑惑,我將告訴你處理它並不像處理普通循環引用那麼簡單。"閉包"被看作函數作用域中的一個臨時對象。一旦函數執行退出,你將失去對閉包本身的引用,那麼你將怎樣去調用detachEvent方法來清除引用呢?在Scott Isaacs的MSN Spaces上有一種解決這個問題的有趣方法。這個方法使用一個額外的引用(原文叫second closure,可是這個示例裏致始致終只有一個closure)協助window對象執行onUnload事件,由於這個額外的引用和閉包的引用存在於同一個對象域中,於是我們可以藉助它來釋放事件引用,從而完成引用移除。爲了簡單起見我們將閉包的引用暫存在一個expando屬性中,下面的示例將向你演示釋放事件引用和清除expando屬性。

   提示:您可以先修改部分代碼再運行

在這篇KB文章中,實際上建議我們除非迫不得已儘量不要創建使用閉包。文章中的示例,給我們演示了非閉包的事件引用方式,即把閉包函數放到頁面的全局作用域中。當閉包函數成爲普通函數後,它將不再繼承其父函數的參數和局部變量,所以我們也就不用擔心基於閉包的循環引用了。在非必要的時候不使用閉包這樣的編程方式可以儘量使我們的代碼避免這樣的問題。

    最後,腳本引擎開發組的Eric Lippert,給我們帶來了一篇關於閉包使用
通俗易懂的好文章。他的最終建議也是希望在真正必要的時候才使用閉包函數。雖然他的文章沒有提及閉包會使用的真正場景,但是這兒已有的大量示例非常有助於大家起步。

頁面交叉泄漏(Cross-Page Leaks)
   這種基於插入順序而常常引起的泄漏問題,主要是由於對象創建過程中的臨時對象未能被及時清理和釋放造成的。它一般在動態創建頁面元素,並將其添加到頁面DOM中時發生。一個最簡單的示例場景是我們動態創建兩個對象,並創建一個子元素和父元素間的臨時域(譯者注:這裏的域(Scope)應該是指管理元素之間層次結構關係的對象)。然後,當你將這兩個父子結構元素構成的的樹添加到頁面DOM樹中時,這兩個元素將會繼承頁面DOM中的層次管理域對象,並泄漏之前創建的那個臨時域對象。下面的圖示示例了兩種動態創建並添加元素到頁面DOM中的方法。在第一種方法中,我們將每個子元素添加到它的直接父元素中,最後再將創建好的整棵子樹添加到頁面DOM中。當一些相關條件合適時,這種方法將會由於臨時對象問題引起泄漏。在第二種方法中,我們自頂向下創建動態元素,並使它們被創建後立即加入到頁面DOM結構中去。由於每個被加入的元素繼承了頁面DOM中的結構域對象,我們不需要創建任何的臨時域。這是避免潛在內存泄漏發生的好方法。

   
Figure 3. DOM插入順序泄漏模型

    接下來,我們將給出一個躲避了大多數泄漏檢測算法的泄漏示例。因爲我們實際上沒有泄漏任何可見的元素,並且由於被泄漏的對象太小從而你可能根本不會注意這個問題。爲了使我們的示例產生泄漏,在動態創建的元素結構中將不得不內聯的包含一個腳本函數指針。在我們設置好這些元素間的相互隸屬關係後這將會使我們泄漏內部臨時腳本對象。由於這個泄漏很小,我們不得不將示例執行成千上萬次。事實上,一個對象的泄漏只有很少的字節。在運行示例並將瀏覽器導航到一個空白頁面,你將會看到兩個版本代碼在內存使用上的區別。當我們使用第一種方法,將子元素加入其父元素再將構成的子樹加入頁面DOM,我們的內存使用量會有微小的上升。這就是一個交叉導航泄漏,只有當我們重新啓動IE進程這些泄漏的內存纔會被釋放。如果你使用第二種方法將父元素加入頁面DOM再將子元素加入其父元素中,同樣運行若干次後,你的內存使用量將不會再上升,這時你會發現你已經修復了交叉導航泄漏的問題。

   提示:您可以先修改部分代碼再運行

這類泄漏應該被澄清,因爲這個解決方法有悖於我們在IE中的一些有益經驗。創建帶有腳本對象的DOM元素,以及它們已進行的相互關聯是瞭解這個泄漏的關鍵點。這實際上這對於泄漏來說是至關重要的,因爲如果我們創建的DOM元素不包含任何的腳本對象,同時使用相同的方式將它們進行關聯,我們是不會有任何泄漏問題的。示例中給出的第二種技巧對於關聯大的子樹結構可能更有效(由於在那個示例中我們一共只有兩個元素,所以建立一個和頁面DOM不相關的樹結構並不會有什麼效率問題)。第二個技巧是在創建元素的開始不關聯任何的腳本對象,所以你可以安全的創建子樹。當你把你的子樹關聯到頁面DOM上後,再繼續處理你需要的腳本事件。牢記並遵守關於循環引用和閉包函數的使用規則,你不會再在掛接事件時在你的代碼中遇到不同的泄漏。

    我真的要指出這個問題,因爲我們可以看出不是所有的內存泄漏都是可以很容易發現的。它們可能都是些微不足道的問題,但往往需要成千上萬次的執行一個更小的泄漏場景才能使問題顯現出來,就像DOM元素插入順序引起的問題那樣。如果你覺得使用所謂的"最佳"經驗來編程,那麼你就可以高枕無憂,但是這個示例讓我們看到,即使是"最佳"經驗似乎也可能帶來泄漏。我們這裏的解決方案希望能提高這些已有的好經驗,或者介紹一些新經驗使我們避免泄漏發生的可能。

貌似泄漏(Pseudo-Leaks)
    在大多數時候,一些APIs的實際的行爲和它們預期的行爲可能會導致你錯誤的判斷內存泄漏。貌似泄漏大多數時候總是出現在同一個頁面的動態腳本操作中,而在從一個頁面跳轉到空白頁面的時候發生是非常少見的。那你怎麼能象排除頁面間泄漏那樣來排除這個問題,並且在新任務運行中的內存使用量是否是你所期望的。我們將使用腳本文本的重寫來作爲一個貌似泄漏的示例。

    象DOM插入順序問題那樣,這個問題也需要依賴創建臨時對象來產生"泄漏"。對一個腳本元素對象內部的腳本文本一而再再而三的反覆重寫,慢慢地你將開始泄漏各種已關聯到被覆蓋內容中的腳本引擎對象。特別地,和腳本調試有關的對象被作爲完全的代碼對象形式保留了下來。

   提示:您可以先修改部分代碼再運行

如果你運行上面的示例代碼並使用任務管理器查看,當從"泄漏"頁面跳轉到空白頁面時,你並不會注意到任何腳本泄漏。因爲這種腳本泄漏完全發生在頁面內部,而且當你離開該頁面時被使用的內存就會回收。對於我們原本所期望的行爲來說這樣的情況是糟糕的。你希望當重寫了腳本內容後,原來的腳本對象就應該徹底的從頁面中消失。但事實上,由於被覆蓋的腳本對象可能已用作事件處理函數,並且還可能有一些未被清除的引用計數。正如你所看到的,這就是貌似泄漏。在表面上內存消耗量可能看起來非常的糟糕,但是這個原因是完全可以接受的。

總結
    每一位Web開發員可能都整理有一份自己的代碼示例列表,當他們在代碼中看到如列表中的代碼時,他們會意識到泄漏的存在並會使用一些開發技巧來避免這些問題。這樣的方法雖然簡單便捷,但這也是今天Web頁面內存泄漏普遍存在的原因。考慮我們所討論的泄漏情景而不是關注獨立的代碼示例,你將會使用更加有效的策略來解決泄漏問題。這樣的觀念將使你在設計階段就把問題估計到,並且確保你有計劃來處理潛在的泄漏問題。使用編寫加固代碼(譯者注:就是異常處理或清理對象等的代碼)的習慣並且採取清理所有自己佔用內存的方法。雖然對這個問題來說可能太誇張了,你也可能幾乎從沒有見到編寫腳本卻需要自己清理自己佔用的內存的情況;使這個問題變得越來越顯著的是,腳本變量和expando屬性間存在的潛在泄漏可能。

    如果對模式和設計感興趣,我強烈推薦Scott的這篇blog,因爲其中演示了一個通用的移除基於閉包泄漏的示例代碼。當然這需要我們使用更多的代碼,但是這個實踐是有效的,並且改進的場景非常容易在代碼中定位並進行調試。類似的注入設計也可以用在基於expando屬性引起的循環引用中,不過需要注意所註冊的方法自身不要讓泄漏(特別使用閉包的地方)跑掉。

About the author

Justin Rogers recently joined the Internet Explorer team as an Object Model developer working on extensibility and previously worked on such notable projects as the .NET QuickStart Tutorials, .NET Terrarium, and SQL Reporting Services Management Studio in SQL Server 2005.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章