其實(shí)在寫這篇文章之前,我也想了很久,因?yàn)榫W(wǎng)上對(duì)這塊的東西已經(jīng)很多了,但有些讀起來(lái)還是不容易讓人理解,而且JS 中的內(nèi)存管理, 我的感覺(jué)就像 JS 中的一門副科, 我們平時(shí)不會(huì)太重視, 但是一旦出問(wèn)題又很棘手. 所以可以通過(guò)平時(shí)多了解一些 JS 中內(nèi)存管理問(wèn)題, 在寫代碼中通過(guò)一些習(xí)慣, 避免內(nèi)存泄露的問(wèn)題。
內(nèi)容概要
- 內(nèi)存的生命周期
- JS的內(nèi)存回收
- 常見的內(nèi)存泄露案例
內(nèi)存生命周期
不管什么程序語(yǔ)言,內(nèi)存生命周期基本是一致的:
- 分配你所需要的內(nèi)存
- 使用分配到的內(nèi)存(讀, 寫)
- 不需要時(shí)將其釋放/歸還
在 C語(yǔ)言中, 有專門的內(nèi)存管理接口, 像malloc() 和 free(). 而在 JS 中, 沒(méi)有專門的內(nèi)存管理接口, 所有的內(nèi)存管理都是"自動(dòng)"的. JS 在創(chuàng)建變量時(shí), 自動(dòng)分配內(nèi)存, 并在不使用的時(shí)候, 自動(dòng)釋放. 這種"自動(dòng)"的內(nèi)存回收, 造成了很多 JS 開發(fā)并不關(guān)心內(nèi)存回收, 實(shí)際上, 這是錯(cuò)誤的.
JS 中的內(nèi)存回收
引用
垃圾回收算法主要依賴于引用的概念. 在內(nèi)存管理的環(huán)境中, 一個(gè)對(duì)象如果有訪問(wèn)另一個(gè)對(duì)象的權(quán)限(隱式或者顯式), 叫做一個(gè)對(duì)象引用另一個(gè)對(duì)象. 例如: 一個(gè)JAVAscript對(duì)象具有對(duì)它原型的引用(隱式引用)和對(duì)它屬性的引用(顯式引用).
引用計(jì)數(shù)垃圾收集
這是最簡(jiǎn)單的垃圾收集算法.此算法把“對(duì)象是否不再需要”簡(jiǎn)化定義為“對(duì)象有沒(méi)有其他對(duì)象引用到它”. 如果沒(méi)有引用指向該對(duì)象(零引用), 對(duì)象將被垃圾回收機(jī)制回收. 示例:
let arr = [1, 2, 3, 4]; arr = null; // [1,2,3,4]這時(shí)沒(méi)有被引用, 會(huì)被自動(dòng)回收
限制: 循環(huán)引用
在下面的例子中, 兩個(gè)對(duì)象對(duì)象被創(chuàng)建并互相引用, 就造成了循環(huán)引用. 它們被調(diào)用之后不會(huì)離開函數(shù)作用域, 所以它們已經(jīng)沒(méi)有用了, 可以被回收了. 然而, 引用計(jì)數(shù)算法考慮到它們互相都有至少一次引用, 所以它們不會(huì)被回收.
function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 引用 o2 o2.p = o1; // o2 引用 o1. 這里會(huì)形成一個(gè)循環(huán)引用 } f();
實(shí)際例子:
var div; window.onload = function(){ div = document.getElementById("myDivElement"); div.circularReference = div; div.lotsOfData = new Array(10000).join("*"); };
在上面的例子里, myDivElement 這個(gè) DOM 元素里的 circularReference 屬性引用了 myDivElement, 造成了循環(huán)引用. IE 6, 7 使用引用計(jì)數(shù)方式對(duì) DOM 對(duì)象進(jìn)行垃圾回收. 該方式常常造成對(duì)象被循環(huán)引用時(shí)內(nèi)存發(fā)生泄漏. 現(xiàn)代瀏覽器通過(guò)使用標(biāo)記-清除內(nèi)存回收算法, 來(lái)解決這一問(wèn)題.
標(biāo)記-清除算法
這個(gè)算法把“對(duì)象是否不再需要”簡(jiǎn)化定義為“對(duì)象是否可以獲得”.
這個(gè)算法假定設(shè)置一個(gè)叫做根root的對(duì)象(在JavaScript里,根是全局對(duì)象). 垃圾回收器將從根開始, 找到所有從根開始引用的對(duì)象, 然后找這些對(duì)象引用的對(duì)象, 從根開始,垃圾回收器將找到所有可以獲得的對(duì)象和所有不能獲得的對(duì)象.
從2012年起, 所有現(xiàn)代瀏覽器都使用了標(biāo)記-清除內(nèi)存回收算法。所有對(duì)JavaScript垃圾回收算法的改進(jìn)都是基于標(biāo)記-清除算法的改進(jìn).
自動(dòng) GC 的問(wèn)題
盡管自動(dòng) GC 很方便, 但是我們不知道GC 什么時(shí)候會(huì)進(jìn)行. 這意味著如果我們?cè)谑褂眠^(guò)程中使用了大量的內(nèi)存, 而 GC 沒(méi)有運(yùn)行的情況下, 或者 GC 無(wú)法回收這些內(nèi)存的情況下, 程序就有可能假死, 這個(gè)就需要我們?cè)诔绦蛑惺謩?dòng)做一些操作來(lái)觸發(fā)內(nèi)存回收.
什么是內(nèi)存泄露?
本質(zhì)上講, 內(nèi)存泄露就是不再被需要的內(nèi)存, 由于某種原因, 無(wú)法被釋放.
常見的內(nèi)存泄露案例
1. 全局變量
function foo(arg) { bar = "some text"; }
在 JS 中處理未被聲明的變量, 上述范例中的 bar時(shí), 會(huì)把bar, 定義到全局對(duì)象中, 在瀏覽器中就是 window 上. 在頁(yè)面中的全局變量, 只有當(dāng)頁(yè)面被關(guān)閉后才會(huì)被銷毀. 所以這種寫法就會(huì)造成內(nèi)存泄露, 當(dāng)然在這個(gè)例子中泄露的只是一個(gè)簡(jiǎn)單的字符串, 但是在實(shí)際的代碼中, 往往情況會(huì)更加糟糕.
另外一種意外創(chuàng)建全局變量的情況.
function foo() { this.var1 = "potential accidental global"; } // Foo 被調(diào)用時(shí), this 指向全局變量(window) foo();
在這種情況下調(diào)用foo, this被指向了全局變量window, 意外的創(chuàng)建了全局變量.
我們談到了一些意外情況下定義的全局變量, 代碼中也有一些我們明確定義的全局變量. 如果使用這些全局變量用來(lái)暫存大量的數(shù)據(jù), 記得在使用后, 對(duì)其重新賦值為 null.
2. 未銷毀的定時(shí)器和回調(diào)函數(shù)
在很多庫(kù)中, 如果使用了觀察著模式, 都會(huì)提供回調(diào)方法, 來(lái)調(diào)用一些回調(diào)函數(shù). 要記得回收這些回調(diào)函數(shù). 舉一個(gè) setInterval的例子.
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerhtml = JSON.stringify(serverData); } }, 5000); // 每 5 秒調(diào)用一次
如果后續(xù) renderer 元素被移除, 整個(gè)定時(shí)器實(shí)際上沒(méi)有任何作用. 但如果你沒(méi)有回收定時(shí)器, 整個(gè)定時(shí)器依然有效, 不但定時(shí)器無(wú)法被內(nèi)存回收, 定時(shí)器函數(shù)中的依賴也無(wú)法回收. 在這個(gè)案例中的 serverData 也無(wú)法被回收.
3. 閉包
在 JS 開發(fā)中, 我們會(huì)經(jīng)常用到閉包, 一個(gè)內(nèi)部函數(shù), 有權(quán)訪問(wèn)包含其的外部函數(shù)中的變量. 下面這種情況下, 閉包也會(huì)造成內(nèi)存泄露.
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // 對(duì)于 'originalThing'的引用 console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
這段代碼, 每次調(diào)用replaceThing時(shí), theThing 獲得了包含一個(gè)巨大的數(shù)組和一個(gè)對(duì)于新閉包someMethod的對(duì)象. 同時(shí) unused 是一個(gè)引用了originalThing的閉包.
這個(gè)范例的關(guān)鍵在于, 閉包之間是共享作用域的, 盡管unused可能一直沒(méi)有被調(diào)用, 但是someMethod 可能會(huì)被調(diào)用, 就會(huì)導(dǎo)致內(nèi)存無(wú)法對(duì)其進(jìn)行回收. 當(dāng)這段代碼被反復(fù)執(zhí)行時(shí), 內(nèi)存會(huì)持續(xù)增長(zhǎng).
該問(wèn)題的更多描述可見Meteor團(tuán)隊(duì)的這篇文章.
4. DOM 引用
很多時(shí)候, 我們對(duì) Dom 的操作, 會(huì)把 Dom 的引用保存在一個(gè)數(shù)組或者 Map 中.
var elements = { image: document.getElementById('image') }; function doStuff() { elements.image.src = 'http://example.com/image_name.png'; } function removeImage() { document.body.removeChild(document.getElementById('image')); // 這個(gè)時(shí)候我們對(duì)于 #image 仍然有一個(gè)引用, Image 元素, 仍然無(wú)法被內(nèi)存回收. }
上述案例中, 即使我們對(duì) image 元素進(jìn)行了移除, 但是仍然有對(duì) image 元素的引用, 依然無(wú)法對(duì)其進(jìn)行內(nèi)存回收.
另外需要注意的一個(gè)點(diǎn)是, 對(duì)于一個(gè) Dom 樹的葉子節(jié)點(diǎn)的引用. 舉個(gè)例子: 如果我們引用了一個(gè)表格中的td元素, 一旦在 Dom 中刪除了整個(gè)表格, 我們直觀的覺(jué)得內(nèi)存回收應(yīng)該回收除了被引用的 td外的其他元素. 但是事實(shí)上, 這個(gè)td 元素是整個(gè)表格的一個(gè)子元素, 并保留對(duì)于其父元素的引用. 這就會(huì)導(dǎo)致對(duì)于整個(gè)表格, 都無(wú)法進(jìn)行內(nèi)存回收. 所以我們要小心處理對(duì)于 Dom 元素的引用.
小結(jié)
我們平時(shí)在寫代碼的時(shí)候,可能很少去操作內(nèi)存管理方面的事情,但我們要有內(nèi)存管理方面的意思,特別是上面我提出的幾種可能導(dǎo)致內(nèi)存泄漏的情況,寫代碼的時(shí)候要謹(jǐn)慎。