JavaScript是一種隱蔽的功能性編程語言,其函數是閉包(封閉):函數對象能夠訪問其封閉做用域中定義的變量,即便該做用域已經完成。 一旦它們定義的函數已經完成,而且在其做用域內定義的全部函數自己都被 GCed(垃圾收集),那麼由閉包捕獲的局部變量就被垃圾收集。javascript
var run = function () { var str = new Array(1000000).join('*'); var doSomethingWithStr = function () { if (str === 'something') console.log("str was something"); }; doSomethingWithStr(); }; setInterval(run, 1000);
咱們將每秒執行一次run
函數。它將分配一個巨大的字符串,建立一個使用它的閉包,調用閉包並返回。返回後,閉包能夠被垃圾收集,str也能夠,由於沒有什麼引用它。可是,若是咱們有一個閉包比run久的呢?html
var run = function () { var str = new Array(1000000).join('*'); var logIt = function () { console.log('interval'); }; setInterval(logIt, 100); }; setInterval(run, 1000);
每隔一秒run
分配一個巨大的字符串,並開始每隔100微秒記錄日誌一次。logIt
永遠都會持續,str
在其詞法做用域內,因此這可能形成內存泄漏!幸運的是,JavaScript實現(或至少是如今的Chrome)足夠聰明能夠注意到在logIt
中沒有使用str,因此它不會被放在logIt
的詞法環境中,並且一旦運行完成,大字符串會被垃圾回收。java
var run = function () { var str = new Array(1000000).join('*'); var doSomethingWithStr = function () { if (str === 'something') console.log("str was something"); }; doSomethingWithStr(); var logIt = function () { console.log('interval'); } setInterval(logIt, 100); }; setInterval(run, 1000);
在Chrome開發者工具中打開「時間軸」選項卡,切換到內存視圖,並點擊記錄: git
![](http://static.javashuo.com/static/loading.gif)
看起來咱們每秒多用額外的兆字節。甚至點擊垃圾桶圖標手動清理垃圾回收也沒有幫助,因此看起來正在泄漏str。(譯註:測試了新版chrome
59.0.3071.115(正式版本)這裏好像沒有出現泄漏)
github
可是這不是和之前同樣嗎?str
僅在run
函數體中引用,在doSomethingWithStr
函數中引用。一旦run
結束 doSomethingWithStr
自己就被清理掉。惟一從run
中泄漏的是第二個閉包,logIt
. 而 logIt
根本沒引用str!chrome
因此即便沒有任何代碼再次引用str,它也不會被垃圾回收器回收。爲何?典型的閉包實現是每一個函數對象有一個連接到一個表示它詞法做用域的字典類型對象。若是定義在run 函數中的兩個函數的確用到str,重要是即便str被分配了一次又一次,這兩個函數共享相同的詞法環境。如今,Chrome的V8 javascript引擎中,若是變量(如例子中的字符串)沒有被閉包引用時,能夠將變量保留在詞法環境以外,這就是第一個例子沒有泄漏的緣由。編程
可是隻要變量被任何閉包使用,它將出如今在該做用域全部閉包共享的詞法環境中。閉包
![](http://static.javashuo.com/static/loading.gif)
你能夠想象一個更聰明的詞法環境實現來避免這個問題。每一個閉包能夠有一個讀取和寫入包含變量的字典(譯註:對象);該字典中的值是能夠 在多個閉包的詞法環境中共享的變異元()。基於我對ECMAScript第5版標準的隨意閱讀,這是合法的:它對詞彙環境的描述將其描述爲「純規範機制(不須要對應於ECMAScript實現的任何具體的文件)」。也就是說,這個標準實際上並不包含「垃圾」一詞,只能說「內存」一次。 編程語言
一旦你注意到這種形式的內存泄漏,修復它們是直接的,如修復Meteor 缺陷中演示的(the fix to the Meteor bug.)。在上面的例子中,很明顯,咱們有意泄漏logIt
而不是str。在原始的Meteor bug,咱們不打算泄漏任何東西:咱們只想用一個新對象代替一個對象,且容許先前的版本被釋放,以下所示: 函數
var theThing = null; var replaceThing = function () { var originalThing = theThing; // Define a closure that references originalThing but doesn't ever actually get called. //定義一個引用originalThing但實際上沒有調用它的實例閉包 // But because this closure exists, originalThing will be in the // lexical environment for all closures defined in replaceThing, instead of // being optimized out of it. If you remove this function, there is no leak. //可是因爲這個閉包的存在,originalThing將存於定義在replaceThing中的全部閉包的詞法環境中,而不被優化,若是你移除 這個方法,就不會有泄漏 var unused = function () { if (originalThing) console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), // While originalThing is theoretically accessible by this function, it // obviously doesn't use it. But because originalThing is part of the // lexical environment, someMethod will hold a reference to originalThing, // and so even though we are replacing theThing with something that has no // effective way to reference the old value of theThing, the old value // will never get cleaned up! //雖然originalThing理論上能夠經過這函數(someMethod)訪問,但顯然沒有使用它,可是因爲originalThing是詞法環境的一部分,someMethod將會保留一個引用指向originalThing,因此即便咱們用非有效的方式替換舊的theThing值,可是舊值不會被清理。 someMethod: function () {} }; // If you add `originalThing = null` here, there is no leak. //此處加originalThing = null不會有泄漏 }; setInterval(replaceThing, 1000);
總結一下:若是你有大對象被一些閉包使用, 而(譯註:這些閉包)不是任何你須要繼續使用的閉包,只要確保當你用大對象的時候,局部變量再也不指向它。不幸的是,這些錯誤可能很是巧妙的(很差發現); 若是JavaScript引擎不須要您考慮它們會更好一些。
原文地址: http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html
相關: http://mrale.ph/blog/2012/09/23/grokking-v8-closures-for-fun.html
附加總結:
1.每一個函數都有一個詞法環境,編譯時產生詞法做用域。
2.函數執行時會產生Context上下文,這個上下文存儲函數中的變量。
3.若是做用域自己嵌套在一個閉包中,那麼新建立的Context 上下文將會指向父對象。 這可能會致使內存泄漏。---> 函數嵌套函數,內部函數將建立一個新上下文.