JavaScript 內(nèi)存管理

簡(jiǎn)介

像C語(yǔ)言這樣的底層語(yǔ)言一般都有底層的內(nèi)存管理接口,比如 malloc()free()。相反,JavaScript是在創(chuàng)建變量(對(duì)象,字符串等)時(shí)自動(dòng)進(jìn)行了分配內(nèi)存,并且在不使用它們時(shí)“自動(dòng)”釋放。 釋放的過(guò)程稱(chēng)為垃圾回收。這個(gè)“自動(dòng)”是混亂的根源,并讓JavaScript(和其他高級(jí)語(yǔ)言)開(kāi)發(fā)者錯(cuò)誤的感覺(jué)他們可以不關(guān)心內(nèi)存管理。

內(nèi)存生命周期

不管什么程序語(yǔ)言,內(nèi)存生命周期基本是一致的:

  1. 分配你所需要的內(nèi)存
  2. 使用分配到的內(nèi)存(讀、寫(xiě))
  3. 不需要時(shí)將其釋放\歸還

所有語(yǔ)言第二部分都是明確的。第一和第三部分在底層語(yǔ)言中是明確的,但在像JavaScript這些高級(jí)語(yǔ)言中,大部分都是隱含的。

JavaScript 的內(nèi)存分配

值的初始化

為了不讓程序員費(fèi)心分配內(nèi)存,JavaScript 在定義變量時(shí)就完成了內(nèi)存分配。

var n = 123; // 給數(shù)值變量分配內(nèi)存
var s = "azerty"; // 給字符串分配內(nèi)存

var o = {
  a: 1,
  b: null
}; // 給對(duì)象及其包含的值分配內(nèi)存

// 給數(shù)組及其包含的值分配內(nèi)存(就像對(duì)象一樣)
var a = [1, null, "abra"];

function f(a){
  return a + 2;
} // 給函數(shù)(可調(diào)用的對(duì)象)分配內(nèi)存

// 函數(shù)表達(dá)式也能分配一個(gè)對(duì)象
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

通過(guò)函數(shù)調(diào)用分配內(nèi)存

有些函數(shù)調(diào)用結(jié)果是分配對(duì)象內(nèi)存:

var d = new Date(); // 分配一個(gè) Date 對(duì)象

var e = document.createElement('div'); // 分配一個(gè) DOM 元素

有些方法分配新變量或者新對(duì)象:

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一個(gè)新的字符串
// 因?yàn)樽址遣蛔兞浚?// JavaScript 可能決定不分配內(nèi)存,
// 只是存儲(chǔ)了 [0-3] 的范圍。

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2);
// 新數(shù)組有四個(gè)元素,是 a 連接 a2 的結(jié)果

使用值

使用值的過(guò)程實(shí)際上是對(duì)分配內(nèi)存進(jìn)行讀取與寫(xiě)入的操作。讀取與寫(xiě)入可能是寫(xiě)入一個(gè)變量或者一個(gè)對(duì)象的屬性值,甚至傳遞函數(shù)的參數(shù)。

當(dāng)內(nèi)存不再需要使用時(shí)釋放

大多數(shù)內(nèi)存管理的問(wèn)題都在這個(gè)階段。在這里最艱難的任務(wù)是找到“哪些被分配的內(nèi)存確實(shí)已經(jīng)不再需要了”。它往往要求開(kāi)發(fā)人員來(lái)確定在程序中哪一塊內(nèi)存不再需要并且釋放它。

高級(jí)語(yǔ)言解釋器嵌入了“垃圾回收器”,它的主要工作是跟蹤內(nèi)存的分配和使用,以便當(dāng)分配的內(nèi)存不再使用時(shí),自動(dòng)釋放它。這只能是一個(gè)近似的過(guò)程,因?yàn)橐朗欠袢匀恍枰硥K內(nèi)存是無(wú)法判定的(無(wú)法通過(guò)某種算法解決)。

垃圾回收

如上文所述自動(dòng)尋找是否一些內(nèi)存“不再需要”的問(wèn)題是無(wú)法判定的。因此,垃圾回收實(shí)現(xiàn)只能有限制的解決一般問(wèn)題。本節(jié)將解釋必要的概念,了解主要的垃圾回收算法和它們的局限性。

引用

垃圾回收算法主要依賴(lài)于引用的概念。在內(nèi)存管理的環(huán)境中,一個(gè)對(duì)象如果有訪問(wèn)另一個(gè)對(duì)象的權(quán)限(隱式或者顯式),叫做一個(gè)對(duì)象引用另一個(gè)對(duì)象。例如,一個(gè)Javascript對(duì)象具有對(duì)它原型的引用(隱式引用)和對(duì)它屬性的引用(顯式引用)。

在這里,“對(duì)象”的概念不僅特指 JavaScript 對(duì)象,還包括函數(shù)作用域(或者全局詞法作用域)。

引用計(jì)數(shù)垃圾收集

這是最初級(jí)的垃圾收集算法。此算法把“對(duì)象是否不再需要”簡(jiǎn)化定義為“對(duì)象有沒(méi)有其他對(duì)象引用到它”。如果沒(méi)有引用指向該對(duì)象(零引用),對(duì)象將被垃圾回收機(jī)制回收。

示例

var o = {
  a: {
    b:2
  }
};
// 兩個(gè)對(duì)象被創(chuàng)建,一個(gè)作為另一個(gè)的屬性被引用,另一個(gè)被分配給變量o
// 很顯然,沒(méi)有一個(gè)可以被垃圾收集

var o2 = o; // o2變量是第二個(gè)對(duì)“這個(gè)對(duì)象”的引用

o = 1;      // 現(xiàn)在,“這個(gè)對(duì)象”只有一個(gè)o2變量的引用了,“這個(gè)對(duì)象”的原始引用o已經(jīng)沒(méi)有

var oa = o2.a; // 引用“這個(gè)對(duì)象”的a屬性
               // 現(xiàn)在,“這個(gè)對(duì)象”有兩個(gè)引用了,一個(gè)是o2,一個(gè)是oa

o2 = "yo"; // 雖然最初的對(duì)象現(xiàn)在已經(jīng)是零引用了,可以被垃圾回收了
           // 但是它的屬性a的對(duì)象還在被oa引用,所以還不能回收

oa = null; // a屬性的那個(gè)對(duì)象現(xiàn)在也是零引用了
           // 它可以被垃圾回收了

限制:循環(huán)引用

該算法有個(gè)限制:無(wú)法處理循環(huán)引用的事例。在下面的例子中,兩個(gè)對(duì)象被創(chuàng)建,并互相引用,形成了一個(gè)循環(huán)。它們被調(diào)用之后會(huì)離開(kāi)函數(shù)作用域,所以它們已經(jīng)沒(méi)有用了,可以被回收了。然而,引用計(jì)數(shù)算法考慮到它們互相都有至少一次引用,所以它們不會(huì)被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

實(shí)際例子

IE 6, 7 使用引用計(jì)數(shù)方式對(duì) DOM 對(duì)象進(jìn)行垃圾回收。該方式常常造成對(duì)象被循環(huán)引用時(shí)內(nèi)存發(fā)生泄漏:

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)引用。如果該屬性沒(méi)有顯示移除或者設(shè)為 null,引用計(jì)數(shù)式垃圾收集器將總是且至少有一個(gè)引用,并將一直保持在內(nèi)存里的 DOM 元素,即使其從DOM 樹(shù)中刪去了。如果這個(gè) DOM 元素?fù)碛写罅康臄?shù)據(jù) (如上的 lotsOfData 屬性),而這個(gè)數(shù)據(jù)占用的內(nèi)存將永遠(yuǎn)不會(huì)被釋放。

標(biāo)記-清除算法

這個(gè)算法把“對(duì)象是否不再需要”簡(jiǎn)化定義為“對(duì)象是否可以獲得”。

這個(gè)算法假定設(shè)置一個(gè)叫做根(root)的對(duì)象(在Javascript里,根是全局對(duì)象)。垃圾回收器將定期從根開(kāi)始,找所有從根開(kāi)始引用的對(duì)象,然后找這些對(duì)象引用的對(duì)象……從根開(kāi)始,垃圾回收器將找到所有可以獲得的對(duì)象和收集所有不能獲得的對(duì)象。

這個(gè)算法比前一個(gè)要好,因?yàn)椤坝辛阋玫膶?duì)象”總是不可獲得的,但是相反卻不一定,參考“循環(huán)引用”。

從2012年起,所有現(xiàn)代瀏覽器都使用了標(biāo)記-清除垃圾回收算法。所有對(duì)JavaScript垃圾回收算法的改進(jìn)都是基于標(biāo)記-清除算法的改進(jìn),并沒(méi)有改進(jìn)標(biāo)記-清除算法本身和它對(duì)“對(duì)象是否不再需要”的簡(jiǎn)化定義。

循環(huán)引用不再是問(wèn)題了

在上面的示例中,函數(shù)調(diào)用返回之后,兩個(gè)對(duì)象從全局對(duì)象出發(fā)無(wú)法獲取。因此,他們將會(huì)被垃圾回收器回收。第二個(gè)示例同樣,一旦 div 和其事件處理無(wú)法從根獲取到,他們將會(huì)被垃圾回收器回收。

限制: 那些無(wú)法從根對(duì)象查詢(xún)到的對(duì)象都將被清除

盡管這是一個(gè)限制,但實(shí)踐中我們很少會(huì)碰到類(lèi)似的情況,所以開(kāi)發(fā)者不太會(huì)去關(guān)心垃圾回收機(jī)制。

今日份的分享來(lái)自MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容