JS(Memory Management) 內存管理

2021-02-14 深圳灣碼農

轉自:https://juejin.cn/post/6898301588356628488#stack

內存的生命周期

在JS中,無論新建一個變量,函數或者其他數據結構,JS引擎會默默的為我們做許多事-為對應的數據結構分配(allocate)內存空間並且在該數據不在被需要的時候回收(deallocate/release)它。

如果大家在對底層語言(C/C++)比較熟悉的話,在C++中利用標準庫,allocate / deallocate 就可以實現內存的手動分配和回收

分配內存是預留內存的過程,而回收內存是將被佔用的內存回收,為其他操作提供空間。

就像人有生老病死,內存也有對應的生命周期。每當賦值變量或者創建函數,內存都會經歷如下的過程:

分配內存
JS引擎為我們分配我們需要操作對象的對應空間回收內存
JS引擎通過特定的算法判斷內存是否需要釋放,而被釋放的內存用於為其他操作提供空間。

在內存管理器中的對象不僅僅包括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);
  });
}

相關焦點

  • win10經常藍屏提示memory management怎麼辦
    當然,它也可能與計算機內存硬體有關。因此,將對具體問題進行詳細分析。Windows10系統藍屏1)首先我們在瀏覽器中搜索並下載名稱是」MemTest64的內存測試軟體」。MemTest64的內存測試工具3)此時檢測界面過程中會不斷地進行刷新當前程序頁面,什麼都不要管,靜等底部出現錯誤的內容信息。
  • Apache Spark 統一內存管理模型詳解
    轉載自 過往記憶(https://www.iteblog.com/) 本文連結:  【Apache Spark 統一內存管理模型詳解】(https://www.iteblog.com/archives/2342.html)本文將對 Spark 的內存管理模型進行分析,下面的分析全部是基於 Apache Spark 2.2.1
  • 郭健:Linux內存管理系統參數配置之OOM(內存耗盡)
    二、什麼是OOMOOM就是out of memory的縮寫,雖然linux kernel有很多的內存管理技巧(從cache中回收、swap out等)來滿足各種應用空間的vm內存需求,但是,當你的系統配置不合理,讓一匹小馬拉大車的時候,linux kernel會運行非常緩慢並且在某個時間點分配page frame的時候遇到內存耗盡、無法分配的狀況
  • JavaScript內存管理機制以及四種常見的內存洩漏解析
    這種看似很「自動化」的資源釋放機制其實是混亂的根源,因為這給JavaScript(以及其他高級語言)開發人員帶來了一種錯覺,認為自己可以不用管理內存。這種想法是錯誤的。即使是使用高級語言,開發人員也應該了解一些內存管理方面的知識(或者至少懂得一些基礎知識)。
  • 絕地求生爆內存怎麼辦 內存不足out of memory解決教程
    今天小編為大家帶來的便是關於遊戲中玩家電腦內存夠的情況下提示內存不足out of memory解決教程,... 絕地求生被玩家吐槽最多的應該就是優化問題,雖然官方對於優化問題非常重視,目前還是有非常多的問題。
  • 每個程式設計師都該了解一點 Linux 內存管理知識
    ,創下了今年閱讀量最低記錄(不算圖片),為了挽回顏面,我決定再寫一篇,還是從案例入手,介紹 Linux 內存管理機制和一個內存溢出殺手的存在感。如果殺掉一個進程還不足以解決問題,Killer 會按照它對進程的評分,逐次幹掉,分數越高,被幹掉的機率越大,直到內存可以正常使用。為什麼會出現這種情況呢?這要從 Linux 的內存管理策略說起。作業系統裡內存管理的主要作用是,進程請求內存的時候為其分配可用內存,進程釋放後回收內存,並監控內存的使用狀況。
  • .NET內存包裝類 Memory 和 Span 相關類型
    參考資料:memory-and-spans --- Microsoft2. 簡介.NET 包含多個相互關聯的類型,它們表示任意內存的連續的強類型區域。所有者, 消費者和生命周期管理由於可以在各個 API 之間傳送緩衝區,以及由於緩衝區有時可以從多個線程進行訪問,因此請務必考慮生命周期管理。下面介紹三個核心概念:以下偽代碼示例闡釋了這三個概念。
  • WinCE內存管理報告
    Windows CE支持虛擬內存動態分配(virtual memory allocation),局部和單獨的堆空間(Local and separate heaps),甚至內存映射文件(memory_mapped files,memory mapping simplifies file access.
  • 絕地求生大逃殺內存不足怎麼辦 絕地求生out of memory解決辦法
    絕地求生大逃殺內存不足怎麼辦 絕地求生out of memory解決辦法時間:2017-10-23 21:09   來源:今日頭條   責任編輯:毛青青 川北在線核心提示:原標題:絕地求生大逃殺內存不足怎麼辦 絕地求生out of memory解決辦法 《絕地求生大逃殺》的配置是很多玩家比較關心的問題,隨著遊戲的火爆程度越來越高
  • 走進C++11(四十)最寬鬆的順序 memory_order_relaxed 內存模型(三)
    之前講的都是理論相關的,下面詳細講一下我們現實中會使用到的內存模型。
  • CF內存不足 out of memory怎麼解決
    玩cf出現out of memory
  • Linux內核中的內存管理
    本文對Linux內存管理使用到的一些數據結構和函數作了簡要描述,而不深入到它們的內部。對這些數據結構和函數有了一個總體上的了解後,再針對各項分別作深入了解的時候,也許會簡單一些。Linux內存訪問限制(僅針對32位系統)默認情況下Linux的內核空間映射到4G虛擬地址的最高1G(即0xC0000000 - 0xFFFFFFF)。
  • Oracle自動內存管理SGA、PGA詳解
    integer 1Gsga_target big integer 1GSYS@PROD>show parameter memoryNAME TYPE VALUE- - hi_shared_memory_address integer 0memory_max_target big integer 0memory_target big
  • 理解NodeJS 的內存管理機制
    v8 的內存限制眾所周知,Node 是基於 v8 引擎來構建的,所以在 Node 中使用的對象基本都是通過 v8 引擎來統一進行內存分配和管理。然而 v8 引擎 本身對內存的使用限制了大小,在64位系統下只能用 1.4GB 的系統內存。
  • 我的世界out of memory怎麼辦 out of memory解決辦法
    ,不知道怎麼回事,當我們遇到我的世界out of memory怎麼辦呢?out of memory在java開發中還是偶爾會遇到,下面我來給你說明下具體原因:   內存是運行電腦時非常關鍵的載體,你的每一步操作都會臨時存儲在內存中,內存有固定大小比如2G,那麼開機時可能你的內存只佔了500M,隨後你打開遊戲,遊戲就會把數據臨時寫到內存中,這時慢慢的內存佔用可能就升到1G了,這是內存升高的原因。
  • 理解memory barrier
    要理解memory barrier必須先知道memory ordered(內存順序)的概念,接觸過一點底層架構的同學大概知道,x86是strongly-ordered,arm是weakly-ordered。
  • 郝健: Linux內存管理學習筆記-第2節課
    圖中內存空間中不同顏色的點都代表一頁內存,無論是高端內存還是低端內存,都有可能被kmalloc,vmalloc和用戶空間的malloc申請走(Buddy算法即是管理這一頁是否被申請走的)。例1:編譯圖上程序,swapoff -a將交換分區關閉,並且配置overcommit_memory為1(允許應用程式申請很大的內存,而內核不再去評估系統中當前還有多少內存可用。)
  • 【文末送書】JavaScript內存管理介紹
    大多數時候,我們在不了解有關內存管理的知識下也只開發,因為 JS 引擎會為我們處理這個問題。不過,有時候我們會遇到內存洩漏之類的問題,這個只有知道內存分配是怎樣工作的,我們才能解決這些問題。內存管理上下文中的「對象」不僅包括JS對象,還包括函數和函數作用域。內存堆和堆棧現在我們知道,對於我們在 JS 中定義的所有內容,引擎都會分配內存並在不再需要內存時將其釋放。我想到的下一個問題是:這些東西將被儲存在哪裡?
  • 家族式管理 family-run management
    隨著市場經濟的發展,以及「富二代」接班等問題的出現,中國越來越多的企業家發現,舊的家族式管理模式已經越來越不適應如今的經濟形勢了。and the old  family-run management style is no longer suitable for the market-oriented business model.
  • 內存初始化代碼分析(三):創建系統內存地址映射
    偌大的物理地址空間中,系統內存佔據了一段或者幾段地址空間,這些信息被保存在了memblock模塊中的memory type類型的數組中,數組中每一個memory region描述了一段系統內存信息(base、size以及node id)。