對於JavaScript這門語言的使用者來講,大多數的使用者的內存管理意識都不強。由於JavaScript一直以來都只做爲在網頁上使用的腳本語言,而網頁每每都不會長時間的運行,因此使用者對JavaScript的運行時長和內存控制都比較漠視。但隨着Spa(單頁應用)、node.js服務端程序和各類js工具的誕生,咱們須要從新重視JavaScript的內存管理。javascript
指因爲疏忽或錯誤形成程序未能釋放已經再也不使用的內存的狀況。內存泄漏並不是指內存在物理上的消失,而是應用程序分配某段內存後,因爲設計錯誤,失去了對該段內存的控制,於是形成了內存的浪費。java
首先JavaScript是一個有Garbage Collection 的語言,也就是咱們不須要手動的回收內存。不一樣的JavaScript引擎有不一樣的垃圾回收機制,這裏咱們主要以V8這個被普遍使用的JavaScript引擎爲主。node
GC根:通常指全局且不會被垃圾回收的對象,好比:window、document或者是頁面上存在的dom元素。JavaScript的垃圾回收算法會判斷某塊對象內存是不是GC根可達(存在一條由GC根對象到該對象的引用),若是不是那這塊內存將會被標記回收。git
做用域:在JavaScript的做用域裏,咱們可以新建對象來分配內存。好比說調用函數,函數執行的過程當中就會建立一塊做用域,若是是建立的是做用域內的局部對象,看成用域運行結束後,全部的局部對象(GC根沒法觸及)都會被標記回收,在JavaScript中能引發做用域分配的有函數調用、with和全局做用域。github
函數調用會建立局部做用域,在局部做用域中的新建的對象,若是函數運行結束後,該對象沒有做用域外部的引用,那該對象將會標記回收web
每一個JavaScript進程都會有一個全局做用域,全局做用域上的引用的對象都是常駐內存的,直到進程退出內存纔會自動釋放。
手動釋放全局做用域上的引用的對象有兩種方式:算法
global.foo = undefinedchrome
從新賦值改變引用緩存
delete global.foo閉包
刪除對象屬性
在JavaScript語言中有閉包的概念,閉包指的是包含自由變量的代碼塊、自由變量不是在這個代碼塊內或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義(局部變量)。
var closure = (function(){ //這裏是閉包的做用域 var i = 0 // i就是自由變量 return function(){ console.log(i++) } })()
閉包做用域會保持對自由變量的引用。上面代碼的引用鏈就是:
window -> closure -> i
閉包做用域還有一個重要的概念,閉包對象是當前做用域中的全部內部函數做用域共享的,而且這個當前做用域的閉包對象中除了包含一條指向上一層做用域閉包對象的引用外,其他的存儲的變量引用必定是當前做用域中的全部內部函數做用域中使用到的變量
將全局變量做爲緩存數據的一種方式,將以後要用到的數據都掛載到全局變量上,用完以後也不手動釋放內存(由於全局變量引用的對象,垃圾回收機制不會自動回收),全局變量逐漸就積累了一些不用的對象,致使內存泄漏
var x = []; function createSomeNodes() { var div; var i = 10000; var frag = document.createDocumentFragment(); for (; i > 0; i--) { div = document.createElement("div"); div.appendChild(document.createTextNode(i + " - " + new Date().toTimeString())); frag.appendChild(div); } document.getElementById("nodes").appendChild(frag); } function grow() { x.push(new Array(1000000).join('x')); createSomeNodes(); setTimeout(grow, 1000); } grow()
上面的代碼貼一張 timeline的截圖
主要看memory區域,經過分析代碼咱們能夠知道頁面上的dom節點是不斷增長的,因此memory裏綠色的線(表明dom nodes)也是不斷升高的;而表明js heap的藍色的線是有升有降,當總體趨勢是逐漸升高,這是由於js 有內存回收機制,每當內存回收的時候藍色的線就會降低,可是存在部份內存一直得不到釋放,因此藍色的線逐漸升高
var nodes = ''; (function () { var item = { name:new Array(1000000).join('x') } nodes = document.getElementById("nodes") nodes.item = item nodes.parentElement.removeChild(nodes) })()
這裏的dom元素雖然已經從頁面上移除了,可是js中仍然保存這對該dom元素的引用。
由於這段代碼是隻執行一次的,因此用timeline視圖會很難分析出來是否存在內存泄漏,因此咱們能夠用 chrome dev tool 的 profile tab裏的heap snapshot 工具來分析。
上面的代碼貼一張 heap snapshot 的summary模式的截圖
經過constructor的filter功能,咱們把上面代碼中建立的長字符串找出來,能夠看到代碼運行結束後,內存中的長字符串依然沒有被垃圾回收掉。
順帶提一下的是右邊紅框裏的shadow size和 retainer size的含義
shadow size 指的是對象本地的大小
retainer size 指的是對象所引用內存的大小,回收該對象是會將他引用的內存也一併回收,因此retainer size 指代的是回收內存後會釋放出來的內存大小
上面咱們能夠看到 長字符串自己的shadow size和retainer size是同樣大的,這是引用長字符串沒有引用其餘的對象,若是有引用其餘對象,那shadow size 和retainer size將不一致。
(function(){ var theThing = null var replaceThing = function () { var originalThing = theThing var unused = function () { if (originalThing) console.log("hi") } theThing = { longStr: new Array(1000000).join('*'), someMethod: function someMethod() { console.log('someMessage') } }; }; setInterval(replaceThing,100) })()
首先咱們明確一下,unused是一個閉包,由於它引用了自由變量 originalThing,雖然它被沒有使用,但v8引擎並不會把它優化掉,由於 JavaScript裏存在eval函數,因此v8引擎並不會隨便優化掉暫時沒有使用的函數。
theThing 引用了someMethod,someMethod這個函數做用域隱式的和unused這個閉包共享一個閉包上下文。因此someMethod也引用了originalThing這個自由變量。
這裏面的引用鏈是:
GCHandler -> replaceThing -> theThing -> someMethod -> originalThing -> someMethod(old) -> originalThing(older)-> someMethod(older)
隨着setInterval的不斷執行,這條引用鏈是不會斷的,因此內存會不斷泄漏,直致程序崩潰。
由於是閉包做用域引發的內存泄漏,這時候最好的選擇是使用 chrome的heap snapshot的container視圖,咱們經過container視圖能清楚的看到這條不斷泄漏內存的引用鏈
因爲做者水平有限,文中若有錯誤還望指出,謝謝!
參考文檔:
百科內存泄漏介紹
chrome devtolls
深刻淺出nodejs
node-interview