??【JS內(nèi)存】正確理解JavaScript閉包與內(nèi)存泄露

JS內(nèi)存生命周期

  1. 分配內(nèi)存;
  2. 放入需要存儲(chǔ)的信息,執(zhí)行內(nèi)存的讀與寫(xiě)操作;
  3. 使用完后釋放內(nèi)存;

【內(nèi)存的分配】Q:JS中不同類(lèi)型的變量是怎么存儲(chǔ)的呢?

JS 中的數(shù)據(jù)類(lèi)型主要有兩類(lèi):基本類(lèi)型和引用類(lèi)型。
JS有兩種不同類(lèi)型的內(nèi)存空間:棧內(nèi)存和堆內(nèi)存。棧是線性表的一種,而堆則是樹(shù)形結(jié)構(gòu)。

  1. 基本類(lèi)型包括:Sting、Number、Boolean、null、undefined、Symbol。這類(lèi)型的數(shù)據(jù)最明顯的特征是大小固定、體積輕量、相對(duì)簡(jiǎn)單,它們被放在 JS 的 棧內(nèi)存 里存儲(chǔ)。
  2. 引用類(lèi)型,比如 Object、Array、Function 等。這類(lèi)數(shù)據(jù)比較復(fù)雜、占用空間較大、且大小不定,它們被放在 JS 的 堆內(nèi)存 里存儲(chǔ)。
let a = 0; 
let b = "Hello World" 
let c = null; 
let d = { name: '修言' }; 
let e = ['修言', '小明', 'bear']; 
變量在內(nèi)存中的形態(tài).png

【內(nèi)存的使用】Q:JS中變量的訪問(wèn)機(jī)制?

在訪問(wèn) a、b、c 三個(gè)變量時(shí),過(guò)程非常簡(jiǎn)單:從棧中直接獲取該變量的值。
在訪問(wèn) d 和 e 時(shí),則需要分兩步走:

  • 從棧中獲取變量對(duì)應(yīng)對(duì)象的引用(即它在堆內(nèi)存中的地址)
  • 拿著 1 中獲取到的地址,再去堆內(nèi)存空間查詢(xún),才能拿到我們想要的數(shù)據(jù)

【內(nèi)存的釋放】垃圾回收機(jī)制

每隔一段時(shí)間,JS 的垃圾收集器就會(huì)對(duì)變量做 “巡檢”。當(dāng)它判斷一個(gè)變量不再被需要之后,它就會(huì)把這個(gè)變量所占用的內(nèi)存空間給釋放掉,這個(gè)過(guò)程叫做 垃圾回收

Q:JS 是如何知道一個(gè)變量是否不被需要的呢?——垃圾回收算法

我們討論的垃圾回收算法有兩種

  1. 引用計(jì)數(shù)法
    這是最初級(jí)的垃圾回收算法,它在現(xiàn)代瀏覽器里幾乎已經(jīng)被淘汰。在引用計(jì)數(shù)法的機(jī)制下,內(nèi)存中的每一個(gè)值都會(huì)對(duì)應(yīng)一個(gè)引用計(jì)數(shù)。當(dāng)垃圾收集器感知到某個(gè)值的引用計(jì)數(shù)為 0 時(shí),就判斷它 “沒(méi)用” 了,隨即這塊內(nèi)存就會(huì)被釋放
  • const arr = [1,2,3] 這段代碼首先是開(kāi)辟了一塊內(nèi)存,把數(shù)組[1,2,3]塞了進(jìn)去,此時(shí)這個(gè)數(shù)組就占據(jù)了一塊內(nèi)存。隨后 arr 變量指向它,這就是創(chuàng)建了一個(gè)指向該數(shù)組的 “引用”。此時(shí)數(shù)組的引用計(jì)數(shù)就是 1。
  • arr = nullarr指向null,這個(gè)數(shù)組[1,2,3]所具備的引用計(jì)數(shù)就會(huì)跟著變成 0,它就變成了一塊沒(méi)用的內(nèi)存,即將面臨著作為 “垃圾” 被回收的命運(yùn)。

引用計(jì)數(shù)法的糟糕點(diǎn)在于無(wú)法甄別循環(huán)引用場(chǎng)景下的垃圾

  1. 標(biāo)記清除法
    考慮到引用計(jì)數(shù)法存在嚴(yán)重的局限性,自 2012 年起,所有瀏覽器都使用了標(biāo)記清除算法??梢哉f(shuō),標(biāo)記清除法是現(xiàn)代瀏覽器的標(biāo)準(zhǔn)垃圾回收算法。
    這個(gè)算法有兩個(gè)階段,分別是標(biāo)記階段和清除階段:
  • 標(biāo)記階段:垃圾收集器會(huì)先找到根對(duì)象,在瀏覽器里,根對(duì)象是 Window;在 Node 里,根對(duì)象是 Global。從根對(duì)象出發(fā),垃圾收集器會(huì)掃描所有可以通過(guò)根對(duì)象觸及的變量,這些對(duì)象會(huì)被標(biāo)記為 “可抵達(dá)”。
  • 清除階段: 沒(méi)有被標(biāo)記為 “可抵達(dá)” 的變量,就會(huì)被認(rèn)為是不需要的變量,這波變量會(huì)被清除

內(nèi)存泄露

該釋放的變量(內(nèi)存垃圾)沒(méi)有被釋放,仍然霸占著原有的內(nèi)存不松手,導(dǎo)致內(nèi)存占用不斷攀高,帶來(lái)性能惡化、系統(tǒng)崩潰等一系列問(wèn)題,這種現(xiàn)象就叫內(nèi)存泄漏。
舉個(gè)例子:

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 'originalThing'的引用
      console.log("嘿嘿嘿");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("哈哈哈");
    }
  };
};
setInterval(replaceThing, 1000);

在 V8 中,一旦不同的作用域位于同一個(gè)父級(jí)作用域下,那么它們會(huì)共享這個(gè)父級(jí)作用域。
在這段代碼里, unused 是一個(gè)不會(huì)被使用的閉包,但和它共享同一個(gè)父級(jí)作用域的 someMethod,則是一個(gè) “可抵達(dá)”(也就意味著可以被使用)的閉包。unused 引用了 originalThing,這導(dǎo)致和它共享作用域的 someMethod 也間接地引用了 originalThing。結(jié)果就是 someMethod “被迫” 產(chǎn)生了對(duì) originalThing 的持續(xù)引用,originalThing 雖然沒(méi)有任何意義和作用,卻永遠(yuǎn)不會(huì)被回收。不僅如此,originalThing 每次 setInterval都會(huì)改變一次指向(指向最近一次的 theThing 賦值結(jié)果),這導(dǎo)致無(wú)法被回收的無(wú)用 originalThing 越堆積越多,最終導(dǎo)致嚴(yán)重的內(nèi)存泄漏。

常見(jiàn)的引發(fā)內(nèi)存泄露的情況

  1. 意外的全局變量
  2. 忘記清除的 setInterval 和 setTimeout
  3. 清除不當(dāng)?shù)腄OM
  4. 閉包

小結(jié)

單純由閉包導(dǎo)致的內(nèi)存泄漏,極少極少。更多的時(shí)候都是編碼的失誤,因此要用嚴(yán)謹(jǐn)?shù)膽B(tài)度對(duì)待每一行代碼!

最后編輯于
?著作權(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)容