JS內(nèi)存生命周期
- 分配內(nèi)存;
- 放入需要存儲(chǔ)的信息,執(zhí)行內(nèi)存的讀與寫(xiě)操作;
- 使用完后釋放內(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)。
- 基本類(lèi)型包括:Sting、Number、Boolean、null、undefined、Symbol。這類(lèi)型的數(shù)據(jù)最明顯的特征是大小固定、體積輕量、相對(duì)簡(jiǎn)單,它們被放在 JS 的 棧內(nèi)存 里存儲(chǔ)。
- 引用類(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)存的使用】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è)變量是否不被需要的呢?——垃圾回收算法
我們討論的垃圾回收算法有兩種
- 引用計(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 = null把arr指向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)景下的垃圾
- 標(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)存泄露的情況
- 意外的全局變量
- 忘記清除的 setInterval 和 setTimeout
- 清除不當(dāng)?shù)腄OM
- 閉包
小結(jié)
單純由閉包導(dǎo)致的內(nèi)存泄漏,極少極少。更多的時(shí)候都是編碼的失誤,因此要用嚴(yán)謹(jǐn)?shù)膽B(tài)度對(duì)待每一行代碼!