【JavaScript】內存管理和垃圾收集機制

簡介

像其他的編程語言都有各自底層的內存管理接口,比如 C 語言的 malloc()free()。但是,JavaScript 是在創建變量時自動分配內存,在不使用時自動釋放,這個釋放過程稱爲垃圾回收。

JavaScript 的這種自動釋放的機制可以讓我們開發者在大部分時間都不需要關心 JavaScript 的內存管理,但話是這麼說,我們還是要了解一下滴。

內存的生命週期

大部分編程語言,內存的生命週期都大概分爲三步:

  1. 分配你所需的內存
  2. 使用分配到的內存(讀、寫)
  3. 不需要時將其釋放

JavaScript 的內存分配

1. 值的初始化

爲了不讓咋們前端程序員分配內存,JavaScript 在定義變量時就完成了內存分配。

let n = 123; // 給數值變量分配內存
let s = "string"; // 給字符串分配內存

// 給對象及其包含的值分配內存
let o = {
  a: 1,
  b: null,
};

// 給數值及其包含的值分配內存(跟對象差不多)
let arr = [1, null, "string", {}];

// 給函數(可調用的對象)分配內存
function f(a) {
  return a * 2;
}

// 函數表達式(回調函數)也能分配一個對象
setTimeout(function(){
    console.log('callBack fun');
},1000)

2.通過函數調用分配內存

有些函數調用的結果是分配對象內存:

const d = new Date() // 分配一個 Date 對象
const e = document.createElement('div') // 分配一個 DOM 元素對象

有些方法返回的結果分配新變量或者新對象內存:

let s = "string";
let s2 = s.substring(0, 3); // s2 是一個新的字符串
// 因爲字符串是不變量
// JavaScript 可能絕對不分配內存
// 只是存儲了 [0-3] 的範圍

let arr = [1, 2];
let arr2 = [3, 4];
let arr3 = arr.concat(arr2)
// arr 數組有四個元素,是 arr 連接 arr2 的結果

使用值(內存)

使用值的過程實際上是對分配內存進行讀取或者寫入的操作。讀取與寫入可能是寫入一個變量或者一個對象的屬性值,甚至是傳遞函數的參數。

釋放內存

當內存不再需要使用時釋放內存,但最難的任務是我們要如何找到“哪些被分配的內存確實是已經不再需要了”,如果沒有垃圾回收機制,就需要開發者手動去確定程序中那一塊內存不再需要了並且主動去釋放它。因此,大部分內存管理的問題都在這個階段。

大部分高級語言都內嵌了“垃圾回收器”,它的主要工作是跟蹤內存的分配和使用,以便當分配的內存不再使用時,自動釋放它。但還是存在一些內存是無法通過垃圾回收器進行跟蹤,因此,上述跟蹤也只是一個近似的過程,而不是百分之百。

垃圾回收算法

因爲內存無法達到百分百跟蹤,因此,垃圾回收實現只能限制的解決一般問題。我們主要需要了解的就是主要的垃圾回收算法和他們的侷限性。

引用的概念

垃圾回收算法主要依賴於引用的概念。在內存管理的環境中,一個對象如果有訪問另外一個對象的權限(顯示、隱式),叫做一個對象引用另一個對象。比如,JavaScript 的原型鏈,就是一個對象具有對它原型的引用(隱式引用)和對它屬性方法的引用(顯示引用)。

在 JavaScript 中,引用“對象”的概念不僅特指 JavaScript 對象,還包括作用域(全局作用域、函數作用域、塊級作用域)。

引用計數算法

最初始的垃圾收集算法,算法很簡單,就是把“對象是否不再需要”簡化定義爲“對象有沒有其他對象引用到它”。如果沒有引用指向該對象,對象就會被垃圾收集機制回收。

事例:

let o = {
  a: {
    b: 2,
  },
};
// 兩個對象被創建,一個作爲另一個的屬性被引用,另一個被分配給變量 o
// 很顯然,沒有一個可以被垃圾收集

let o2 = o; // o2 變量是第二個對“這個對象”的引用

o = 1; // 現在,“這個對象”只有一個 o2 變量的引用了,“這個對象”的原始引用 o 已經沒有

let oa = o2.a // 引用“這個對象”的 a 屬性;現在,“這個對象”有兩個引用了,一個是 o2,一個是 oa

o2 = 'my_new_o2' // 雖然最初的對象現在已經是零引用了,可以被垃圾回收了; 但是它的屬性 a 的對象還在被 oa 引用,所以還不能回收

oa = null // a 屬性的那個對象現在也是零引用了; 它可以被垃圾回收了

但是引用計數存在一個很嚴重的問題,那就是循環引用。循環引用的情況對象無法被垃圾回收機制回收。比如下面的例子,兩個對象被創建,並相互引用,形成了一個循環。他們被調用之後會離開函數作用域,所以他們應該已經沒有用到了,理論上應該被回收了。但是,引用計數算法考慮到它們互相之間至少有一次引用,所以他們不會被回收。

function f() {
  let o = {};
  let o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o
  return "success";
}
f();

正是因爲上面的限制,早期 IE 瀏覽器用引用計數的方式對 DOM 對象進行垃圾回收時,經常造成對象被循環引用從而造成內存泄漏問題。

let div;
window.onload = function () {
  div = document.getElementById("myDivElement");
  div.circularReference = div; // 這裏循環引用了
  div.lotsOfData = new Array(10000).join("*");
};

標記清除算法

這個算法把“對象是否不再需要”簡化定義爲“對象是否可以獲得”。

這個算法會假定設置一個叫做根(root)的對象(在 JavaScript 中,根是全局對象 Global)。垃圾回收器將定期聰根開始,找所有從根開始引用的對象,然後找到這些對象引用的對象...以此類推,從根開始,垃圾回收器將找到所有可以獲得到的對象和收集所有不能獲得的對象。

image

這個算法比引用計數算法較好,因爲在引用計數中,“有零引用的對象”總是不可獲取的,但是相反卻不一定,參考循環引用;而標記清除因爲必須要獲得,相互引用的兩個對象是無法通過第三方引用獲得的。

從 2012 年起,所有現代瀏覽器都使用了標記清除垃圾回收算法。所有對 JavaScript 垃圾回收算法的改進都是基於標記清除算法的改進,並沒有改進標記清除算法本身和它對“對象是否不再需要”的簡化定義。

深入我們可以瞭解下 V8 對 GC 的優化。

V8 對 GC 的優化

待更新...

參考文獻

內存管理 - JavaScript | MDN
js垃圾回收機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章