轉自:https://juejin.cn/post/6898301588356628488#stack
內存的生命周期在JS中,無論新建一個變量,函數或者其他數據結構,JS引擎會默默的為我們做許多事-為對應的數據結構分配(allocate)內存空間並且在該數據不在被需要的時候回收(deallocate/release)它。
如果大家在對底層語言(C/C++)比較熟悉的話,在C++中利用標準庫,allocate / deallocate 就可以實現內存的手動分配和回收
分配內存是預留內存的過程,而回收內存是將被佔用的內存回收,為其他操作提供空間。
就像人有生老病死,內存也有對應的生命周期。每當賦值變量或者創建函數,內存都會經歷如下的過程:
分配內存在內存管理器中的對象不僅僅包括JS對象,而且還囊括了函數和函數作用域。
JS中一切皆對象
我們從上文得知,JS引擎分配空間並在該空間不被引用的時候將其回收。
那這些被分配的空間具體被存放在哪裡呢?
JS引擎有兩個地方用於存放數據:堆(memory heap)和棧(stack)
堆和棧是JS引擎存放數據的兩個不同的數據結構。
棧:靜態內存分配上圖,所有的值都被存放到 stack 中-->由於他們的類型都是原始類型(primitive)
stack 是JS用於存放靜態數據的數據結構。靜態數據是一種JS引擎在編譯階段能準確知道該數據大小的數據類型。在JS中,靜態數據包括原始類型(string/number/boolean/undefined/null)和引用類型(指向對象和函數的指針)。
由於JS引擎知道它們的大小不會發生更改,所以每次都會為其分配指定大小的內存空間。
在代碼執行之前分配內存的過程被稱為 靜態內存分配。
由於JS引擎為這些數據分配定值的內存空間,所以在stack存儲的數據是有內存上限的。而該上限由不同瀏覽器各自決定。
堆:動態內存分配heap 是JS用於存放對象和函數的地方。
不像stack,JS引擎不會為這些對象分配定值的內存空間。相反,這些空間是按需分配的。
該分配內存的方式被稱為動態內存分配。
為了便於區分stack 和heap各自的區別,繪製如下的表格:
區別StackHeap存儲類型原始類型和引用類型對象和函數內存是否定值在編譯階段已經確定內存大小在運行階段確定大小存儲數據是否存在內存上限是(不同瀏覽器規則不同)不存在內存上限示例const person = {
name: 'John',
age: 24,
};JS為這個對象在heap中分配空間。然而其屬性的值為原始類型,所以屬性值被存儲在stack中。
const hobbies = ['hiking', 'reading'];數組也屬於對象,所以它也是被存儲在heap中。
let name = 'John'; // 為string 分配內存
const age = 24; // 為數字分配內存
name = 'John Doe'; // 重新分配內存
const firstName = name.slice(0,4); // 重新分配內存原始數據是不可變(immutable)的。為了將原來的值進行替換,JS會重新創建一個新的值。
JS中的引用所有變量都在stack中存在指定的信息。在上文中我們得知,stack中存儲兩種類型的值- 原始類型和引用類型。原始類型我們不用過多解釋,而引用類型需要著重解釋一下。
由於對象和函數存放在heap中,而heap是一個雜亂無章的數據結構,沒有指定的順序,所以JS解釋器在從上到下編譯代碼的時候,就會按照數據出現的順序,依次按照數據類型存放到指定的位置。而在發現某個數據是非原始類型,就會在stack中存儲其在heap中存儲的引用地址。
垃圾回收通過上文的學習,我們已經知道了JS引擎是如何存儲不同的數據,但是凡事都是有頭有尾的,既然存在內存的分配,那勢必就會存在內存的回收。
和內存分配一樣,內存回收的工作JS也為我們代勞了。並且還派專人(GC (garbage collector))全權負責此事。
一旦JS引擎發現變量或者函數不在被引用,GC將其佔用的空間釋放(deallocate/release)掉。
而回收內存最主要的問題是,內存是否被引用是一個不可預知的事,這也意味著沒有一種算法能夠在內存不被佔用的時候,將所有的內存回收。
下面我們討論一些比較典型的垃圾回收算法。
引用計數這是一種最簡單的方式。它通過回收那些沒有指針指向的對象。
通過上面的一系列操作,雖然將 person和 newPerson的引用都給置為 null,但是對象中 hobbies的還是被其他變量所引用。
循環引用針對引用計數這種GC方式,有一種情況是無法處理的-循環引用。當一個或者多個對象互相引用,但是這些對象已經處於孤立狀態。此時,引用計數就手足無措了。
let son = {
name: 'John',
};
let dad = {
name: 'Johnson',
}
son.dad = dad;
dad.son = son;
son = null;
dad = null;雖然在不使用son 和dad 的時候,將它們都置為null。這只是將它們與stack中的引用脫離的關係,但是在heap中,他們還彼此引用。
son 和dad 對象互相引用,而引用計數的算法在這兩個對象沒有被其他對象引用的時候是無法釋放內存的。
標記清除標記清除算法能夠很好的解決循環引用的痛點。不同於引用計數通過計算對象引用個數的方式來決定是否進行垃圾回收,它採用了一種通過從根對象開始遍歷,如果某個對象遍歷不到,那就需要被GC釋放對應的內存。
在瀏覽器中,這個根對象是window對象,而在NodeJS中,是global對象。
該算法通過 標記那些不能被訪問到的對象,作為GC的目標。
內存洩漏
根元素不參與標記過程!通過上文的介紹和對已有概念的掌握,我們來分析一些常規的可能造成內存洩漏的原因。
將變量掛載到全局變量將數據肆無忌憚的存儲在全局變量上,這是一種很常見的內存洩漏。
在瀏覽器環境中,如果在定義一個變量的時候,預設了var/const/let,此時定義的變量就會被掛載到window對象上。
users = getUsers();我們可以通過以strict mode來運行代碼。
如果在某些情況下,逼不得已需要將變量掛載到全局變量上,你需要在該變量使命完成的時候,手動將其置為null。
window.users = null;
關閉定時器和清除回調函數忘記清除定時器和回調函數,將會使項目的代碼越來越大。尤其針對SPA(單頁面應用),在動態添加定時器和回調的時候要格外小心。
及時關閉定時器const object = {};
const intervalId = setInterval(function() {
// everything used in here can't be collected
// until the interval is cleared
doSomething(object);
}, 2000);上面的代碼,每2m執行一次,定時器中的變量在定時器沒有關閉的時候,是一直無法被GC的。
所以,在不使用定時器的時候,需要將其銷毀。
clearInterval(intervalId);
清除回調函數const element = document.getElementById('button');
const onClick = () => alert('hi');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
清除DOM 引用將DOM元素存儲到JS中,可能也會導致內存洩漏。
當你想通過數組來銷毀元素,這種情況是無法觸發GC操作的
const elements = [];
const element = document.getElementById('button');
elements.push(element);
function removeAllElements() {
elements.forEach((item, index) => {
document.body.removeChild(document.getElementById(item.id));
elements.splice(index, 1);
});
}