JavaScript內存管理介紹

做者:Ahmad shaded
譯者:前端小智
來源:felixgerschau
點贊再看,養成習慣

本文 GitHub https://github.com/qq44924588... 上已經收錄,更多往期高贊文章的分類,也整理了不少個人文檔,和教程資料。歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。javascript

大多數時候,咱們在不瞭解有關內存管理的知識下也只開發,由於 JS 引擎會爲咱們處理這個問題。不過,有時候咱們會遇到內存泄漏之類的問題,這個只有知道內存分配是怎樣工做的,咱們才能解決這些問題。前端

在本文中,主要介紹內存分配垃圾回收的工做原理以及如何避免一些常見的內存泄漏問題。java

緩存( Memory)生命週期

在 JS 中,當咱們建立變量、函數或任何對象時,J S引擎會爲此分配內存,並在再也不須要時釋放它。git

分配內存是在內存中保留空間的過程,而釋放內存則釋放空間,準備用於其餘目的。github

每次咱們分配一個變量或建立一個函數時,該變量的存儲會經歷如下相同的階段:面試

clipboard.png

分配內存算法

  • JS 會爲咱們處理這個問題:它分配咱們建立對象所需的內存。

使用內存數組

  • 使用內存是咱們在代碼中顯式地作的事情:對內存的讀寫其實就是對變量的讀寫。

釋放內存瀏覽器

  • 此步驟也由 JS 引擎處理,釋放分配的內存後,就能夠將其用於新用途。
內存管理上下文中的「對象」不只包括JS對象,還包括函數和函數做用域。

內存堆和堆棧

如今咱們知道,對於咱們在 JS 中定義的全部內容,引擎都會分配內存並在再也不須要內存時將其釋放。緩存

我想到的下一個問題是:這些東西將被儲存在哪裏?

JS 引擎在兩個地方能夠存儲數據:內存堆堆棧。堆和堆棧是引擎是用於不一樣目的的兩個數據結構。

堆棧:靜態內存分配

全部值都存儲在堆棧中,由於它們都是基本類型的值

堆棧是 JS 用於存儲靜態數據的數據結構。 靜態數據是引擎在編譯時能知道大小的數據。 在 JS 中,包括指向對象和函數的原始值(stringsnumberbooleanundefinednull)和引用類型。

因爲引擎知道大小不會改變,所以它將爲每一個值分配固定數量的內存。

在執行以前當即分配內存的過程稱爲靜態內存分配。這些值和整個堆棧的限制取決於瀏覽器。

堆:動態內存分配

是另外一個存儲數據的空間,JS 在其中存儲對象函數

與堆棧不一樣,JS 引擎不會爲這些對象分配固定數量的內存,而根據須要分配空間。這種分配內存的方式也稱爲動態內存分配

下面將對這兩個存儲的特性進行比較:

堆棧
存放基本類型和引用
大小在編譯時已知
分配固定數量的內存
對象和函數
在運行時才知道大小
沒怎麼限制

事例

來幾個事例,增強一下映像。

const person = {
  name: 'John',
  age: 24,
};

JS 在堆中爲這個對象分配內存。實際值仍然是原始值,這就是它們存儲在堆棧中的緣由。

const hobbies = ['hiking', 'reading'];

數組也是對象,這就是爲何它們存儲在堆中的緣由。

let name = 'John'; // 爲字符串分配內存
const age = 24; // 爲字分配內存

name = 'John Doe'; // 爲新字符串分配內存
const firstName = name.slice(0,4); // 爲新字符串分配內存

始值是不可變的,因此 JS 不會更改原始值,而是建立一個新值。

JavaScript 中的引用

全部變量首先指向堆棧。 若是是非原始值,則堆棧包含對中對象的引用。

堆的內存沒有按特定的方式排序,因此咱們須要在堆棧中保留對其的引用。 咱們能夠將引用視爲地址,並將堆中的對象視爲這些地址所屬的房屋。

請記住,JS 將 對象函數存儲在堆中。 基本類型和引用存儲在堆棧中。

clipboard.png

這張照片中,咱們能夠觀察到如何存儲不一樣的值。 注意personnewPerson都如何指向同一對象。

事例

const person = {
  name: 'John',
  age: 24,
};

這將在堆中建立一個新對象,並在堆棧中建立對該對象的引用。

垃圾回收

如今,咱們知道 JS 如何爲各類對象分配內存,可是在內存生命週期,還有最後一步:釋放內存

就像內存分配同樣,JavaScript引擎也爲咱們處理這一步驟。 更具體地說,垃圾收集器負責此工做。

一旦 JS 引擎識別變量或函數不在被須要時,它就會釋放它所佔用的內存。

這樣作的主要問題是,是否仍然須要一些內存是一個沒法肯定的問題,這意味着不可能有一種算法可以在再也不須要那一刻當即收集再也不須要的全部內存。

一些算法能夠很好地解決這個問題。 我將在本節中討論最經常使用的方法:引用計數標記清除算法。

引用計數

當聲明瞭一個變量並將一個引用類型值賦值該變量時,則這個值的引用次數就是1。若是同一個值又被賦給另一個變量,則該值得引用次數加1。相反,若是包含對這個值引用的變量又取 得了另一個值,則這個值的引用次數減 1

當這個值的引用次數變成 0時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那 些引用次數爲零的值所佔用的內存。

咱們看下面的例子。

圖片描述

請注意,在最後一幀中,只有hobbies留在堆中的,由於最後引用的是對象。

週期數

引用計數算法的問題在於它不考慮循環引用。 當一個或多個對象互相引用但沒法再經過代碼訪問它們時,就會發生這種狀況。

let son = {
  name: 'John',
};

let dad = {
  name: 'Johnson',
}

son.dad = dad;
dad.son = son;

son = null;
dad = null;

clipboard.png

因爲父對象相互引用,所以該算法不會釋放分配的內存,咱們再也沒法訪問這兩個對象。

它們設置爲null不會使引用計數算法識別出它們再也不被使用,由於它們都有傳入的引用。

標記清除

標記清除算法對循環依賴性有解決方案。 它檢測到是否能夠從root 對象訪問它們,而不是簡單地計算對給定對象的引用。

瀏覽器的rootwindow 對象,而NodeJS中的rootglobal

clipboard.png

該算法將沒法訪問的對象標記爲垃圾,而後對其進行掃描(收集)。 根對象將永遠不會被收集。

這樣,循環依賴關係就再也不是問題了。在前面的示例中,dad對象和son 對象都不能從根訪問。所以,它們都將被標記爲垃圾並被收集。

自2012年以來,該算法已在全部現代瀏覽器中實現。 僅對性能和實現進行了改進,算法的核心思想仍是同樣的。

折衷

自動垃圾收集使咱們能夠專一於構建應用程序,而不用浪費時間進行內存管理。 可是,咱們須要權衡取捨。

內存使用

因爲算法沒法確切知道何時再也不須要內存,JS 應用程序可能會使用比實際須要更多的內存。

即便將對象標記爲垃圾,也要由垃圾收集器來決定什麼時候以及是否將收集分配的內存。

若是你但願應用程序儘量提升內存效率,那麼最好使用低級語言。 可是請記住,這須要權衡取捨。

性能

收集垃圾的算法一般會按期運行以清理未使用的對象。

問題是咱們開發人員不知道什麼時候會回收。 收集大量垃圾或頻繁收集垃圾可能會影響性能。然而,用戶或開發人員一般不會注意到這種影響。

內存泄漏

在全局變量中存儲數據,最多見內存問題多是內存泄漏

在瀏覽器的 JS 中,若是省略varconstlet,則變量會被加到window對象中。

users = getUsers();

在嚴格模式下能夠避免這種狀況。

除了意外地將變量添加到根目錄以外,在許多狀況下,咱們須要這樣來使用全局變量,可是一旦不須要時,要記得手動的把它釋放了。

釋放它很簡單,把 null 給它就好了。

window.users = null;

被遺忘的計時器和回調

忘記計時器和回調可使咱們的應用程序的內存使用量增長。 特別是在單頁應用程序(SPA)中,在動態添加事件偵聽器和回調時必須當心。

被遺忘的計時器

const object = {};
const intervalId = setInterval(function() {
  // 這裏使用的全部東西都沒法收集直到清除`setInterval`
  doSomething(object);
}, 2000);

上面的代碼每2秒運行一次該函數。 若是咱們的項目中有這樣的代碼,頗有可能不須要一直運行它。

只要setInterval沒有被取消,則其中的引用對象就不會被垃圾回收。

確保在再也不須要時清除它。

clearInterval(intervalId);

被遺忘的回調

假設咱們向按鈕添加了onclick偵聽器,以後該按鈕將被刪除。舊的瀏覽器沒法收集偵聽器,可是現在,這再也不是問題。

不過,當咱們再也不須要事件偵聽器時,刪除它們仍然是一個好的作法。

const element = document.getElementById('button');
const onClick = () => alert('hi');

element.addEventListener('click', onClick);

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

脫離DOM引用

內存泄漏與前面的內存泄漏相似:它發生在用 JS 存儲DOM元素時。

const elements = [];
const element = document.getElementById('button');
elements.push(element);

function removeAllElements() {
  elements.forEach((item) => {
    document.body.removeChild(document.getElementById(item.id))
  });
}

刪除這些元素時,咱們還須要確保也從數組中刪除該元素。不然,將沒法收集這些DOM元素。

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);
  });
}

因爲每一個DOM元素也保留對其父節點的引用,所以能夠防止垃圾收集器收集元素的父元素和子元素。

總結

在本文中,咱們總結了 JS 中內存管理的核心概念。寫這篇文章能夠幫助咱們理清一些咱們不徹底理解的概念。

但願這篇對你有所幫助,咱們下期再見,記得三連哦!


代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:https://felixgerschau.com/jav...

交流

文章每週持續更新,能夠微信搜索「 大遷世界 」第一時間閱讀和催更(比博客早一到兩篇喲),本文 GitHub https://github.com/qq449245884/xiaozhi 已經收錄,整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,另外關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索