1. 程式人生 > >深入理解js記憶體機制

深入理解js記憶體機制

原文連結:深入理解js記憶體機制

js的記憶體機制在很多前端開發者看來並不是那麼重要,但是如果你想深入學習js,並將它利用好,打造高質量高效能的前端應用,就必須要了解js的記憶體機制。對於記憶體機制理解了以後,一些基本的問題比如最基本的引用資料型別和引用傳遞到底是怎麼回事兒?比如淺複製與深複製有什麼不同?還有閉包,原型等等就迎刃而解了。

js型別

在js中,js的型別分為兩個大類,分別是基本資料型別引用資料型別。我們暫時先拋開ES6不說,先只說在ES5中的型別。在ES5中有5中簡單資料型別(也就是上面說的基本資料型別):Undefined、Null、Boolean、Number和String。還有1種複雜的資料型別————Object,Object本質上是由一組無序的名值對組成的。其中可以算在object中的還有Array和Function。

在記憶體當中,基本資料型別存放在棧中,引用資料型別存放在堆中。說到這裡就要說一下記憶體空間了,一般來說,js的記憶體空間分為棧(stack)、堆(heap)、池(一般也會歸類棧中)。其中棧存放變數,堆存放複雜物件,池存放常量,所以也叫常量池。

但是有一個變數是特殊的,那就是閉包中的變數,閉包中的變數並不儲存中棧記憶體中,而是儲存在堆記憶體中,這也就解釋了函式之後之後為什麼閉包還能引用到函式內的變數。

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}

閉包的簡單定義是:函式 A 返回了一個函式 B,並且函式 B 中使用了函式 A 的變數,函式 B 就被稱為閉包。

函式 A 彈出呼叫棧後,函式 A 中的變數這時候是儲存在堆上的,所以函式B依舊能引用到函式A中的變數。現在的 JS 引擎可以通過逃逸分析辨別出哪些變數需要儲存在堆上,哪些需要儲存在棧上。

引用資料型別與堆記憶體

與其他語言不同,JS的引用資料型別,比如陣列Array,它們值的大小是不固定的。引用資料型別的值是儲存在堆記憶體中的物件。JavaScript不允許直接訪問堆記憶體中的位置,因此我們不能直接操作物件的堆記憶體空間。在操作物件時,實際上是在操作物件的引用而不是實際的物件。因此,引用型別的值都是按引用訪問的。這裡的引用,我們可以粗淺地理解為儲存在變數物件中的一個地址,該地址與堆記憶體的實際值相關聯。

為了更好的搞懂變數物件與堆記憶體,我們可以結合以下例子與圖解進行理解.

var b = { m: 20 }; // 變數b存在棧中,對應的值就是一個索引指向物件{m: 20},{m:20}作為物件存在於堆記憶體中.
深入理解js記憶體機制

因此當我們要訪問堆記憶體中的引用資料型別時,實際上我們首先是從變數物件中獲取了該物件的地址引用(或者地址指標),然後再從堆記憶體中取得我們需要的資料。

如此,就會出現我們經常被問到的關於引用資料型別的值問題了

var x =30;
var b = x;
x+=10;
console.log(b);

上面的問題我們很容易解答,答案就是會輸出30,x的操作不會對b有什麼影響,因為在變數物件中的資料發生複製行為時,系統會自動為新的變數分配一個新值。var b = a執行之後,a與b雖然值都等於20,但是他們其實已經是相互獨立互不影響的值了。但是下面這個問題就有意思了

var x={m:1}
var y = x;
x.m++;
console.log(y.m);

通過輸出我們發現答案是2。這是因為我們通過var y = x是執行一次複製引用型別的操作。引用型別的複製同樣也會為新的變數自動分配一個新的值儲存在變數物件中,但不同的是,這個新的值,僅僅只是引用型別的一個地址指標。當地址指標相同時,儘管他們相互獨立,但是在變數物件中訪問到的具體物件實際上是同一個。

垃圾回收

在js中有垃圾回收機制,其作用是回收過期無效的變數,以防止記憶體洩漏。這些工作不需要我們去管理什麼時候進行垃圾回收,js會自動進行,這讓我們寫起程式碼來感覺超級爽,哈哈。

下面來看一下js垃圾回收機制什麼時候會回收變數。我們寫程式碼的時候是區分全域性變數和區域性變數的,在此,我們看一下區域性變數和全域性變數的銷燬。

  • 區域性變數:區域性作用域中,當函式執行完畢,區域性變數也就沒有存在的必要了,因此垃圾收集器很容易做出判斷並回收。
  • 全域性變數:全域性變數什麼時候需要自動釋放記憶體空間則很難判斷,所以在開發中儘量避免使用全域性變數,以提高記憶體有效使用率

垃圾回收演算法

現在各大瀏覽器通常用採用的垃圾回收有兩種方法:標記清除、引用計數。

引用計數

現代瀏覽器基本上已經不再使用了,在這裡我們做一下簡單的介紹。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明瞭一個變數並將一個引用型別賦值給該變數時,則這個值的引用次數就是1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其所佔的記憶體空間給收回來。這樣,垃圾收集器下次再執行時,它就會釋放那些引用次數為0的值所佔的記憶體。簡單來說就是看一個物件是否有指向它的引用。如果沒有其他物件指向它了,說明該物件已經不再需要了。

// 建立一個物件person,他有兩個指向屬性age和name的引用
var person = {
    age: 12,
    name: 'aaaa'
};
person.name = null; // 雖然name設定為null,但因為person物件還有指向name的引用,因此name不會回收
var p = person;
person = 1;         //原來的person物件被賦值為1,但因為有新引用p指向原person物件,因此它不會被回收
p = null;           //原person物件已經沒有引用,很快會被回收

但是,如果出現了迴圈引用,那麼這種方式就會存在一個大bug,看一下下面這個例子

function bigBug(){
    var objA = new Object();
    var objB = new Object();
    objA.bug1 = objB;
    objB.bug2 = objA;
}

在上面這個例子中,ab兩個物件是互相引用的,也就是說他們的引用次數永遠為2,如果不進行其他操作的話,這樣的互相引用如果大量使用的話,就會造成記憶體洩漏問題。雖然說現在主流的瀏覽器都已經不在使用了,但是之前的IE版本還是那樣,所以在寫程式碼時我們應該儘量避免。避免的方法就是在不使用的時候進行手動解除迴圈繫結

objA.bug1 = null;
objB.bug2 = null;

標記清除

標記清除演算法將“不再使用的物件”定義為“無法到達的物件”。即從根部(在JS中就是全域性物件)出發定時掃描記憶體中的物件,凡是能從根部到達的物件,保留。那些從根部出發無法觸及到的物件被標記為不再使用,稍後進行回收。每一個變數都有自己的使用環境,當進入環境以後,垃圾回收機制就會給他打上一個“進入環境”的標籤,從邏輯上來將,系統不能清除處於環境中的變數,因為只要是在環境中就有可能會使用到。當其離開環境時,會給其打上“離開環境”標籤,這時候便可以進行回收了。

記憶體洩漏

記憶體洩漏可能對於前端開發著來說比較陌生,但是你肯定遇到過瀏覽器卡死的現象,卡死的原因就可能是因為一個死迴圈導致的記憶體爆滿洩漏。所以對於持續執行的服務程序(daemon),必須及時釋放不再用到的記憶體。否則,記憶體佔用越來越高,輕則影響系統性能,重則導致程序崩潰。 對於不再用到的記憶體,沒有及時釋放,就叫做記憶體洩漏(memory leak)
如果想模擬的話,可以按下面的步驟來進行操作:

  • 開啟開發者工具,選擇 Memory
  • 在右側的Select profiling type欄位裡面勾選 timeline
  • 點選左上角的錄製按鈕。
  • 在頁面上進行各種操作,模擬使用者的使用情況。
  • 一段時間後,點選左上角的 stop 按鈕,面板上就會顯示這段時間的記憶體佔用情況。

深入理解js記憶體機制