這篇文章其實已經看了有些日子了,並且最近的一些開發都在盡量的遵循文中的原則。可是目前的情況是代碼規模稍微大點以後,IE的記憶體流失還是很嚴重,於是我非常生氣(倒沒啥後果)覺得該把這篇文章挖出來批批。為了方便批鬥,所以決定先給翻譯成中文,結果在精讀以後,發現每個泄漏情景的描述和避免,作者幾乎都留了一手,所以這麼看來文章又都對了,沒啥可批的啦。只是讓我想起啦真的劉一手。。。
Author: Justin Rogers ,Micrsoft Corporation June 2005
Translator by: http://birdshome.cnblogs.com
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對象來引發並展現它。
<html>
<head>
<script language="JScript">
var myGlobalObject;
function SetupLeak()
{
// First set up the script scope to element reference
myGlobalObject = document.getElementById("LeakedDiv");
// Next set up the element to script scope reference
document.getElementById("LeakedDiv").expandoProperty = myGlobalObject;
}
function BreakLeak()
{
document.getElementById("LeakedDiv").expandoProperty = null;
}
</script>
</head>
<body onload="SetupLeak()" onunload="BreakLeak()">
<div id="LeakedDiv"></div>
</body>
</html>
你可以使用直接賦null值得方式來破壞該泄漏情形。在頁面文檔卸載前賦null值,將會讓指令碼引擎知道對象間的引用鏈沒有了。現在它將能正常的清理引用並釋放DOM對象。在這個樣本中,作為Web開發員的你因該更多的瞭解了對象間的關係。
作為一個基本的情形,循環參考可能還有更多不同的複雜表現。對基於對象的JScript,一個通常用法是通過封裝JScript對象來擴充DOM對象。在構建過程中,你常常會把DOM對象的引用放入JScript對象中,同時在DOM對象中也存放上對新近建立的JScript對象的引用。你的這種應用模式將非常便於兩個對象之間的相互訪問。這是一個非常直接的循環參考問題,但是由於使用不用的文法形式可能並不會讓你在意。要破環這種使用情景可能變得更加複雜,當然你同樣可以使用簡單的樣本以便於清楚的討論。
<html>
<head>
<script language="JScript">
function Encapsulator(element)
{
// Set up our element
this.elementReference = element;
// Make our circular reference
element.expandoProperty = this;
}
function SetupLeak()
{
// The leak happens all at once
new Encapsulator(document.getElementById("LeakedDiv"));
}
function BreakLeak()
{
document.getElementById("LeakedDiv").expandoProperty = null;
}
</script>
</head>
<body onload="SetupLeak()" onunload="BreakLeak()">
<div id="LeakedDiv"></div>
</body>
</html>
更複雜的辦法還有記錄所有需要解除引用的對象和屬性,然後在Web文檔卸載的時候統一清理,但大多數時候你可能會再造成額外的泄漏情形,而並沒有解決你的問題。
to be continued ...
// Closure我沒有翻,他的表現是內建函式,並且可以訪問父函數的變數,有人翻成閉包被罵啦一頭包。