首頁  >  文章  >  web前端  >  詳解JavaScript中的記憶體管理

詳解JavaScript中的記憶體管理

青灯夜游
青灯夜游轉載
2021-01-06 10:14:352240瀏覽

詳解JavaScript中的記憶體管理

相關推薦:《javascript影片教學

#大多時候,我們在不了解記憶體管理的知識下也只開發,因為JS 引擎會為我們處理這個問題。不過,有時候我們會遇到記憶體洩漏之類的問題,而這個只有知道記憶體分配是怎麼運作的,我們才能解決這些問題。

在本文中,主要介紹記憶體分配垃圾回收的工作原理以及如何避免一些常見的記憶體洩漏問題。

快取( Memory)生命週期

在 JS 中,當我們建立變數、函數或任何物件時,J S引擎會為此分配內存,並在不再需要時釋放它。

分配記憶體是在記憶體中保留空間的過程,而釋放記憶體則釋放空間,準備用於其他目的。

每次我們分配一個變數或建立一個函數時,該變數的儲存都會經歷以下相同的階段:

詳解JavaScript中的記憶體管理

分配記憶體

  • JS 會為我們處理這個問題:它分配我們創建物件所需的記憶體。

使用記憶體

  • 使用記憶體是我們在程式碼中明確地做的事情:對記憶體的讀寫其實就是對變數的讀寫。

釋放記憶體

  • 此步驟也由 JS 引擎處理,釋放分配的記憶體後,就可以用於新用途。

記憶體管理上下文中的「對象」不僅包括JS對象,還包括函數和函數作用域。

記憶體堆疊和堆疊

現在我們知道,對於我們在 JS 中定義的所有內容,引擎都會分配記憶體並在不再需要記憶體時將其釋放。

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

JS 引擎在兩個地方可以儲存資料:記憶體堆疊堆疊。堆和堆疊是引擎是用於不同目的的兩個資料結構。

堆疊:靜態記憶體分配

詳解JavaScript中的記憶體管理

堆疊是 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 將物件函數儲存在堆中。基本類型和參考存儲在堆疊中。

詳解JavaScript中的記憶體管理

這張照片中,我們可以觀察到如何儲存不同的值。注意personnewPerson都如何指向同一物件。

事例

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

這將在堆中建立一個新對象,並在堆疊中建立對該對象的參考。

垃圾回收

現在,我們知道 JS 如何為各種物件分配內存,但是在記憶體生命週期,還有最後一步:釋放記憶體

就像記憶體分配一樣,JavaScript引擎也為我們處理這一步驟。更具體地說,垃圾收集器負責此工作。

一旦 JS 引擎識別變數或函數不在被需要時,它就會釋放它所佔用的記憶體。

這樣做的主要問題是,是否仍然需要一些記憶體是一個無法確定的問題,這意味著不可能有一種演算法能夠在不再需要那一刻立即收集不再需要的所有記憶體。

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

引用計數

當宣告了一個變數並將一個引用型別值賦值該變數時,則這個值的參考次數就是1。如果同一個值又被賦給另一個變量,則該值得引用次數加1。相反,如果包含這個值引用的變數又取 得了另外一個值,則這個值的引用次數減 1

當這個值的引用次數變成 0時,則表示沒有辦法再存取這個值了,因而就可以將其佔用的記憶體空間回收回來。這樣,當垃圾收集器下次再運作時,它就會釋放那 些引用次數為零的值所佔用的記憶體。

我們看下面的例子。

詳解JavaScript中的記憶體管理

請注意,在最後一幀中,只有hobbies留在堆中的,因為最後引用的是物件。

週期數

引用計數演算法的問題在於它不考慮循環引用。當一個或多個物件互相引用但無法再透過程式碼存取它們時,就會發生這種情況。

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

let dad = {
  name: 'Johnson',
}

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

son = null;
dad = null;

詳解JavaScript中的記憶體管理

由於父物件相互引用,因此演算法不會釋放分配的內存,我們再也無法存取這兩個物件。

它們設定為null不會使引用計數演算法識別它們不再被使用,因為它們都有傳入的參考。

標記清除

標記清除演算法對循環依賴性有解決方案。它檢測到是否可以從root 物件存取它們,而不是簡單地計算對給定物件的參考。

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

詳解JavaScript中的記憶體管理

此演算法將無法存取的物件標記為垃圾,然後對其進行掃描(收集)。根物件將永遠不會被收集。

這樣,循環依賴關係就不再是問題了。在前面的範例中,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 中内存管理的核心概念。写这篇文章可以帮助我们理清一些我们不完全理解的概念。

希望这篇对你有所帮助,我们下期再见,记得三连哦!

原文地址:https://felixgerschau.com/javascript-memory-management/

作者:Ahmad shaded

译文地址:https://segmentfault.com/a/1190000037651993

更多编程相关知识,请访问:编程入门!!

以上是詳解JavaScript中的記憶體管理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除