javaScript 記憶體管理機制

雲叔_又拍雲 發表於 2022-04-14
JavaScript

大家好,今天分享的主題為 JavaScript 記憶體管理機制,本次分享將從以下三部分進行講述:

  • js 記憶體管理與 js 垃圾

  • 常見的 GC 演算法

  • V8 引擎的垃圾回收

js 記憶體管理與 js 垃圾

關於 JavaScript 記憶體管理機制,相信大家都有所瞭解。我們就簡單看一下 js 記憶體管理與 js 垃圾。JavaScript 記憶體管理是由 JS 自動操作的,不需要人為進行參與,這些記憶體管理包含以下三項:

  • 申請記憶體空間

  • 使用內容空間

  • 釋放內容空間

而 js 垃圾是指物件不在引用時、物件不能從根上訪問到時,都可以被稱為 js 垃圾。其他部分包括引用和可達物件這些大家肯定很熟悉了,我們就不再多說。下面我們談一談 GC 演算法。

GC 演算法

GC 演算法其實是為了找到記憶體中的垃圾,並釋放和回收空間。這裡所說的的垃圾,是指演算法中認為程式中不再需要使用的物件,與程式中不能訪問到的物件。

說回 GC 演算法,這個是比較概念性的內容,我們簡單歸納一下。GC 是一種自記憶體中查詢垃圾釋放空間、回收空間的一個垃圾回收器機制。演算法則是工作時查詢和回收所遵循的規則。常見 GC 演算法有引用計數、標記清除、標記整理、分代回收。

引用計數

引用計數曾經主要用於 IE8 以下的瀏覽器,現在的瀏覽器已不再使用,因此只做簡單介紹。引用計數的基本原理是記錄跟蹤每一個值被引用的次數,被引用則計數加一,被釋放則減一,當數值為零時則代表該值所在記憶體已經不再使用,因此釋放所佔空間。引用計數的優點是引用次數實時監控,所以回收垃圾能夠及時回收,從而最大限度減少程式暫停卡頓時間。但也是因為一直在運作,所以資源消耗和時間開銷大,無法回收迴圈引用的物件。

標記清除

標記清除分為分為標記和清除兩個階段,其核心思想是遍歷所有物件,找標記活動物件,即前面提到的可達物件,清除沒有標記的物件,以及回收沒有標記物件的空間。

javaScript 記憶體管理機制

上圖是 global 的查詢簡易流程圖。其中左側 A、B、C、D、E 表示可查到的物件,右側 a1、b1 表示迴圈引用物件。其中 a1 為引用計數,而因為引用計數一直在運作,無法回收迴圈引用的物件的缺點,可以反向找到正在迴圈引用的物件。

這也是標記清除的優點,可以解決物件迴圈的引用回收問題。但是標記清除的缺點是空間碎片化,無法及時回收垃圾物件。因為它需要先標記再清除,不能像引用計數一樣對值進行實時監控,因此無法讓空間最大化使用。通過下圖可以簡單看一下標記清除的空間碎片化特點。

javaScript 記憶體管理機制

標記整理

上面提到標記清除有空間碎片化的缺點,而標記整理優化了這個缺點。從名字也可以聯想到,標記整理是標記清除的增強。標記整理在標記階段的操作和標記清除一致,但是在清除階段會先執行整理,再進行清除。這種方式能夠有效減少碎片化空間。和標記清除一樣,標記整理也不能實時回收垃圾物件。

我們通過下面三張圖對標記整理進行一個簡單直觀的瞭解。

javaScript 記憶體管理機制

可以看到在進行垃圾回收前,活動空間和非活動空間是混雜的。而在確定進行回收後,標記整理會對空間進行歸類整理,將活動空間和非活動空間統一整理到一起,形成下圖的結果:

javaScript 記憶體管理機制

之後再進行標記清除就能夠避免回收操作避免出現大量碎片化空間,讓空間最大化應用。

javaScript 記憶體管理機制

看完了 GC 演算法,以 V8 引擎為例我們具體來看一下 GC 演算法在 JS 垃圾回收裡的使用。

V8 引擎的垃圾回收

V8 是一款當下較為主流 JavaScript 執行引擎,採用即時編譯,處理速度很快。V8 的記憶體是設限的,比如 64 位作業系統的上限是 1.4T,下限是 700M,32 位作業系統的上下限分別為 64M 和 32M。

V8 採用分代回收的垃圾回收策略,將記憶體分為新生代和舊生代兩種,並對不同的物件採用不同的對應演算法。

V8 的垃圾回收策略

javaScript 記憶體管理機制

上圖是 V8 的記憶體分配示意圖,可以清除看到 V8 記憶體空間分為兩部分。左邊的 from 和 to 是新生代,佔用的空間比較小(32M|16M),這裡的新生代指的是存活時間短的儲存區。右邊紅色的部分則是存活時間較長的老生代儲存區。

V8 常用的 GC 演算法有以下 5 種:

  • 分代回收

  • 空間複製

  • 標記清除

  • 標記整理

  • 標記增量

這其中新生代採用複製演算法和標記整理進行垃圾回收,老生代使用標記清除、標記整理和增量標記進行垃圾回收。

V8 新生代物件回收實現

javaScript 記憶體管理機制

上圖為 V8 新生代物件回收實現圖,採用複製演算法和標記整理結合的方式進行垃圾回收。新生代記憶體區的兩個等大空間,From 代表使用空間用於儲存活動物件,To 代表空閒空間。V8 的新生代物件回收是通過標記整理將物件完成整理後拷貝到 To,然後將 To 和 From 進行空間交換,並釋放整理後的無用物件所佔空間。需要注意的是,在將整理物件拷貝到 To 時可能會出現晉升。晉升指的是將新生代物件移動至老生代儲存區。晉升通常有兩個條件,其一是在進行一輪 GC 後還活著的新生代物件可以晉升,其二是 To 空間的使用率超過 25%。

V8 老生代物件回收實現

V8 老生代的回收過程採用標記清除、標記整理和標記增量結合的方式。一般在進行垃圾回收時會通過標記清除完成垃圾空間的回收,但是當新生代移動到老生代,而老生代記憶體不夠時,則會通過標記整理進行空間優化,並使用增量標記進行效率優化。

增量標記示意圖

標記增量其實是通過對標記操作進行標記的方法,讓時間安排變得合理。這句話可能有些繞,簡單說就是在垃圾回收時,讓標記系統在標記時分出不同的時間段,分別進行標記和執行,讓二者的操作間隔開,從而優化時間安排,這會讓頁面在體感上更為順暢。

推薦閱讀

OpenShift 與 OpenStack:讓雲變得更簡單

如何處理大體積 XLSX/CSV/TXT 檔案?