1. 程式人生 > >[ Javascript ] 內存泄露以及循環引用解析

[ Javascript ] 內存泄露以及循環引用解析

設置 name 簡單 卸載 uid dsm 版本號 導致 mod

內存泄露

在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 IE

xhr.open(‘GET‘, ‘/server.url‘, true)

xhr.onreadystatechange = function() {
  if(xhr.readyState == 4 && xhr.status == 200) {            
    // ...
  }
}

xhr.send(null)

看一下內存結構:

技術分享


這個異步xmlHttpRequest對象一直被瀏覽器追蹤,由於有一個內部的引用關聯到它。

當這個請求結束之後,這個引用就會被刪除,所以xhr 變成不可關聯對象。可是ie9下面的瀏覽器不是這麽做的。

幸運的是,要修復這個Bug非常easy。我們須要刪除這個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‘) // set
alert( $(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 ] 內存泄露以及循環引用解析