標籤:記憶體泄露 瀏覽器 javascript 循環參考 記憶體管理
記憶體泄露
在javascript中,我們很少去關注記憶體的管理。我們建立變數,使用變數,瀏覽器關注這些底層的細節都顯得很正常。
但是當應用程式變得越來越複雜並且ajax化之後,或者使用者在一個頁面停留過久,我們可能需要去注意一些問題,如一個瀏覽器花費了1G以上的記憶體,並且在不斷的增加。這些問題常常都是因為記憶體泄露引起。
Javascript 記憶體泄露
這個javascript記憶體管理的核心概念就是具不具有可達性的概念。
1 一個明顯的對象集合將會被認為是可達的:這些對象是被知道的像roots一樣。
包括那些所有的對象在堆棧中個地方被引用(包括,所有的局部變數,正在被調用的方法的中的參數),以及任何的全域變數。
2 對象儲存在記憶體中,他們是可以到達的從roots 對象,通過一個引用貨者一個引用鏈。
這裡有一個GC 記憶體回收行程在瀏覽器中,用來清楚沒有用的對象在記憶體中。
記憶體回收example
function Menu(title) { this.title = title this.elem = document.getElementById('id')}var menu = new Menu('My Menu')document.body.innerHTML = '' // (1)menu = new Menu('His menu') // (2)
來看一下記憶體結構:
在step(1) 中,Body.innerHTML 被清除掉,所以它的子節點也會被刪除,因為他們不再被關聯。
但是這個元素#id 是一個例外,他是是被 menu.elem 關聯著,所以該對象會一直存在記憶體中,當然 ,如果你檢查他的parentNode, 將會得到一個null值。
注意:個別的Dom元素 可以會儲存在記憶體中即使他們的parent 被移除了。
在step(2) 中,引用window.menu 被定義,所以之前的 menu因為不再被關聯,它將會自動被移除通過瀏覽器的GC。
循環參考集合
閉包經常會導致循環參考,例如:
function setHandler() { var elem = document.getElementById('id') elem.onclick = function() { // ... }}
在這裡,這個DOM 元素直接引用匿名function通過onclick。並且這個function引用了elem元素通過外部的詞法環境。
( 這裡多說一點,關於[[Scope]]是function的內部屬性,在建立function的時候,會將外部函數的詞法環境加入到[[Scope]]中,這裡涉及到javascript的範圍問題。)
這樣的記憶體結構一樣會出現即使這個處理函數內部沒有任何的代碼。特別的一些方法如addEventListener/attachEvent 也會在內部建立一個引用。
在這個處理函數中通常進行清除,當這個elem死亡的時候。
function cleanUp() { var elem = document.getElementById('id') elem.parentNode.removeChild(elem)}
調用clearUp刪除元素從Dom 中。這裡依舊存在一個引用,LexialEnvironment.elem ,但是這裡沒有了嵌套的functions,所以 LexialEnvironment 是可以回收的。
在這之後,elem 變成沒有關聯的並且和他的handlers一起被回收。
記憶體泄露
記憶體泄露主要發生當一些瀏覽器由於一些問題不能夠移除沒有用的對象從記憶體中。
這發生可能是由於一些原因,如瀏覽器的Bugs,瀏覽器的擴充問題,或多或少,我們自己的代碼錯誤。
IE 8 以下 DOM-JS 記憶體泄露
IE8 之前的瀏覽器不能對DOM和javascript之間的循環參考進行清理。這個問題相對更加的嚴重在ie6 windows xp sp3 之前的版本
因為記憶體沒法釋放在頁面卸載之前。
所以 setHandler 泄露在ie 8 之前的瀏覽器,elem 和這些閉包沒辦法清除。
function setHandler() { var elem = document.getElementById('id') elem.onclick = function() { /* ... */ }}
不僅僅是DOM 元素,包括XMLHttpRequest 或者其它COM 物件,都會存在此現象。
在IE下用來打破循環參考的方法:
我們定義了elem = null,所以這個處理函數不再關聯到DOM 元素,這個迴圈自然打破。
XmlHttpRequest 記憶體管理和泄露
下面的代碼在i9以下瀏覽器記憶體泄露:
var xhr = new XMLHttpRequest() // or ActiveX in older IExhr.open('GET', '/server.url', true)xhr.onreadystatechange = function() { if(xhr.readyState == 4 && xhr.status == 200) { // ... }}xhr.send(null)
看一下記憶體結構:
這個非同步xmlHttpRequest對象一直被瀏覽器追蹤,因為有一個內部的引用關聯到它。
當這個請求結束之後,這個引用就會被刪除,所以xhr 變成不可關聯對象。但是ie9以下的瀏覽器不是這麼做的。
幸運的是,要修複這個Bug很簡單,我們需要刪除這個xhr 從這個閉包中並且使用它用this在這個處理函數中。如下:
var xhr = new XMLHttpRequest() xhr.open('GET', 'jquery.js', true) xhr.onreadystatechange = function() { if(this.readyState == 4 && this.status == 200) { document.getElementById('test').innerHTML++ }} xhr.send(null)xhr = null}, 50)
這樣就沒有了循環參考。
setInterval/setTimeout
在使用setTimeout/setInterval 也會存在內部引用並且被追蹤知道結束,然後clear up.
對於setInterval 這個結束髮生在 clearInterval中,這個可能會導致記憶體泄露當這個方法實際什麼也 沒做,但是這個interval卻沒有被清除。
記憶體泄露的大小
記憶體泄露的資料結構的size可能不大。
但是這個閉包會導致外部函數的所有的變數遺留下來,當這個內建函式是活動的時候。
所以,你可以想象,你建立了一個function,而且其中一個變數包含了一個大的字串。
function f() { var data = "Large piece of data, probably received from server" /* do something using data */ function inner() { // ... } return inner}
While the function inner function stays in memory, then the LexicalEnvironment with a large variable inside will hang in memory until the inner function is alive.
事實上,這可能沒有泄露,許多的fucntions 可能會被建立因為一些合理的原因。比如,對於每一個請求,並不清乾淨,因為他們是一些處理函數或者其它什麼。
如果這個data 僅僅被使用在外部函數,我們可以使它作廢在外部方法中。
function f() { var data = "Large piece of data, probably received from server" /* do something using data */ function inner() { // ... }data = null return inner}
現在。這個data 依舊保留在記憶體中作為一個詞法環境的一個屬性,不過它不再需要去佔用太多的空間。
jQuery 記憶體泄露和避免方式
jQuery 使用 $.data 去避免ie 6 7 記憶體泄露。不幸運的是,它導致了一些新的 jQuery 特殊的記憶體泄露。
這個核心原理關於$.data是,任何的javascript實體被限制去讀取一個元素使用如下的方式
// works on this site cause it's using jQuery$(document.body).data('prop', 'val') // setalert( $(document.body).data('prop') ) // get
jQuery $(elem).data(prop,val) 按照如下步驟:
1 元素擷取一個唯一的標記如果它不存在的話:
elem[ jQuery.expando ] = id = ++jQuery.uuid // from jQuery source
2 data 被設定到一個特殊的對象 jQuery.cache:
jQuery.cache[id]['prop'] = val
當這個date從一個元素中被讀取:
1 這個元素的唯一標示會被返回:id = elem[jQuery.expando]
2 這個data 會被讀取從jQuery.cache[id]
jQuery設定這個api的目的就是為了讓DOM元素不再直接引用Javascript元素。它使用了一個數量,但是很安全。
這個data 儲存在jQuery.cache中。內部事件處理函數同樣使用$.data API。
同時也造成了另一方面的影響,一個元素不能被移除從DOM中使用 本地的調用。
如下代碼造成了記憶體泄露在所有的瀏覽器中:
$('<div/>') .html(new Array(1000).join('text')) // div with a text, maybe AJAX-loaded .click(function() { }) .appendTo('#data')document.getElementById('data').innerHTML = ''
這個泄露的發生因為elem 被removeed 通過清除 parent 的innerHTML .但是這個data依舊儲存在jQuery.cache中。
更重要的是,這個事件處理函數引用elem,所以這個事件處理函數和elem保留在記憶體中和整個閉包。
一個簡單的泄露例子
function go() { $('<div/>') .html(new Array(1000).join('text')) .click(function() { })}
這個例子的問題在於,這個元素被建立了,但是沒有使用。所以在這個函數定義之後,這個引用就消失了, 但是這個jQuery.cache中依舊是存在的。
----------------------------------------------------------------------------
原文地址:http://javascript.info/tutorial/memory-leaks
[ Javascript ] 記憶體泄露以及循環參考解析