1. 程式人生 > >V8垃圾回收?看這篇就夠了!

V8垃圾回收?看這篇就夠了!

什麼是記憶體管理

記憶體管理是控制協調應用程式訪問電腦記憶體的過程。這個過程是複雜的,對於我們來說,可以說相當於一個黑匣子。

當咱們的應用程式執行在某個作業系統中的時候,它訪問電腦記憶體(RAM)來達成下列幾個功能:

  1. 執行需要執行的位元組碼(程式碼)
  2. 儲存程式執行時候所需的資料
  3. 載入程式執行所需的執行時環境

上面用來儲存程式執行時所需的資料,就是下面要說的堆(heap)和棧(stack)。

棧(stack)

顧名思義,是一種先進後出的結構,參考一下餐盤的取和放。

俄羅斯套娃,我這不禁

棧的特點

  • 由於先進後出性質,在資料的處理上棧有著很好的速度,因為只需從最頂部壓棧和出棧就好了,簡單明瞭。
  • 不過,儲存在棧中的資料必須是大小有限,生存期確定。
  • 函式執行的時候會建立一個明確的棧,並壓入,而當執行期間會儲存函式內的所有資料,這就是棧幀。個人感覺可以理解為當前執行函式的快照。
  • 多執行緒應用程式有多個棧。
  • 棧的作業系統自動分配或釋放。
  • 儲存在棧中的常見型別有:區域性變數(值型別、基本型別、常量)、指標和函式
  • 還記得平常偶爾遇到的stack overflow error嗎?這是因為與堆相比,棧的大小受到了限制。大多屬語言都是都是這樣。

堆(heap)

堆常用來動態記憶體分配,程式在堆中尋找資料需要使用指標。

堆的特點

  • 效率不如棧,但是可以儲存更多的資料
  • 可儲存大小不確定的資料,如執行時確定
  • 應用程式中多執行緒共享堆資料
  • 堆由人工操作,故管理起來很棘手,可能會引起記憶體洩漏等問題,所以有很多語言有gc機制
  • 儲存在堆中的常見型別有:全域性變數、引用型別和其他複雜的資料結構
  • 這就是為什麼你會遇到out of memory errors這類問題,因為使用者的胡亂分配或者未銷燬
  • 我們分配給堆的資料其實並沒有大小限制,理論上來說你可以分配無窮大的資料。當然如果這樣你也得為應用程式分配這麼多的記憶體。 -_-

記憶體堆疊 與 資料結構堆疊的區別

記憶體 資料結構
new一個物件的引用或地址儲存在棧區,指向該物件儲存在堆區中的真實資料。由程式設計師分配和回收 是一棵完全二叉樹結構
儲存執行方法的形參、區域性變數、返回值。由系統自動分配和回收。 是一種連續儲存的資料結構,特點是儲存的資料先進後出

記憶體管理的重要性

與硬碟不同,記憶體大小有限的,並且往往不是很大。所以說,如果在沒有釋放無用記憶體的情況下,無休止的繼續消耗記憶體空間,那麼就會造成記憶體不足甚至作業系統崩潰。。。
所以,以為這種情況,許多語言都提供自動記憶體管理。並且由於棧是由系統操控,所以我們接下來討論的自動記憶體管理主要是堆。

寫到這,我想說,我有點不認識堆這個字了。。。

不同的記憶體管理方法

因為現在語言開發者們不想給使用者增加記憶體管理的負擔(或者說是不相信他們能處理好。。),所以設計出自動管理記憶體的方式。並且很多語言設計出多種方法去自動管理記憶體,以供開發者選擇。下面一一介紹。

手動記憶體管理

語言預設不管理記憶體,比如C、C++,他們提供malloc、realloc、calloc和free等,但是這不是適用於所有人。

GC (Garbage collection)

通過釋放無用的記憶體空間去管理堆記憶體,gc是當前程式設計中最常見的一種記憶體自動管理方法,不過大量處理會造成執行緒卡主,所以利用碎片時間進行。
使用gc的語言有JVM、JavaScript、C#、Golang、OCaml和ruby。

Mark & Sweep GC

又名: tracing GC。有兩個階段,第一階段標記那些應該處於活躍狀態的資料,下個階段釋放不被引用的資料。
例如JVM、C#、RubyJavaScript和Golang都採用這種方法。
V8引擎還會使用Reference counting GC去彌補,這種gc也可以用於C、C++作為外部庫,

Reference counting GC

引用計數記憶體管理。使用數字代表當前資料是否可回收,數字會根據引用和失去引用而改變,當為0的時候即被回收。但是這種無法處理迴圈引用。

Resource Acquisition is Initialization (RAII)

由c++之父Bjarne Stroustrup提出,中文翻譯為資源獲取即初始化。
這種型別的記憶體管理,物件的記憶體分配與它的宣告週期有關,其核心是把資源和物件的生命週期繫結,物件建立獲取資源,物件銷燬釋放資源。在RAII的指導下,C++把底層的資源管理問題提升到了物件生命週期管理的更高層次。在C++中引入,Ada和Rust也有在用。

Automatic Reference Counting(ARC)

自動引用計數。是蘋果公司的Objective-C程式的一種自動記憶體管理機制。
類似於引用計數,但是代替以特定間隔執行,將保留和釋放命令插入到程式碼中,當計數為0時,自動觸發, 無需程式暫停。當然ARC依然不能處理迴圈引用。需要開發者使用某些關鍵字去處理。

Ownership

所有權是Rust的突破功能. 它使Rust可以完全記憶體安全且高效,同時避免垃圾回收。
Rust語言的ownership是rust語言的核心,rust語言之所以被稱之為安全的面向系統級別的程式語言 正是由此特性決定的。

rust指南

小結

到目前為止,我們簡單介紹了記憶體管理的相關內容,每個語言都有特定的機制,統統不同的演算法達到不同的目標,接下里我們來聊聊V8.

V8引擎

V8在執行之前將JavaScript編譯成了機器程式碼,而非位元組碼或是解釋執行它,以此提升效能。V8使用C++書寫,可以嵌入到任何C++應用程式中。

V8記憶體結構

首先,讓我們看下V8引擎記憶體結構長什麼樣:

由於js是單執行緒的,所以node依然會為每個js環境提供單執行緒環境。如果你在服務端使用,他會為每個服務提供一個程序。在V8程式中,應用程式始終被分配的記憶體代表。這種記憶體成為常駐集。如上圖所示。

V8中的堆記憶體

這裡用來儲存物件和動態資料,這是記憶體中最大的區域,並且是GC工作的地方。不過,並不是所有的堆記憶體都可以進行GC,只有新生代和老生代被gc管理。堆可以進一步細分為下面這樣:

  • 新生代空間:是最新產生的資料存活的地方,這些資料往往都是短暫的。這個空間被一分為二,然後被Scavenger(Minor GC)所管理。稍後會介紹。可以通過V8標誌如 --max_semi_space_size 或 --min_semi_space_size 來控制新生代空間大小
  • 老生代空間:是從新生代空間經過至少兩輪Minor GC仍然存活下來的資料,該空間被Major GC(Mark-Sweep & Mark-Compact)管理,稍後會介紹。可以通過 --initial_old_space_size 或 --max_old_space_size控制空間大小。
    1. Old pointer space: 存活下來的包含指向其他物件指標的物件
    2. Old data space: 存活下來的只包含資料的物件。
  • 大物件空間: 這是比空間大小還要大的物件,大物件不會被gc處理。
  • 程式碼空間:這裡是JIT所編譯的程式碼。這是除了在大物件空間中分配程式碼並執行之外的唯一可執行的空間。
  • map空間:存放 Cell 和 Map,每個區域都是存放相同大小的元素,結構簡單。

V8記憶體管理剖析

到目前為止,我們大概瞭解了記憶體的各空間組織方式,接下來,讓我們看看當程式執行時記憶體的重要性。

少bb,看程式碼:

class Employee {
    constructor(name, salary, sales) {
        this.name = name;
        this.salary = salary;
        this.sales = sales;
    }
}

const BONUS_PERCENTAGE = 10;

function getBonusPercentage(salary) {
    const percentage = (salary * BONUS_PERCENTAGE) / 100;
    return percentage;
}

function findEmployeeBonus(salary, noOfSales) {
    const bonusPercentage = getBonusPercentage(salary);
    const bonus = bonusPercentage * noOfSales;
    return bonus;
}

let john = new Employee("John", 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
console.log(john.bonus);

點選此處檢視程式碼執行過程中,記憶體空間的展示

大白話解釋一下:
全域性向下文像一個快照一樣儲存在棧上,每一次函式呼叫也會將一個快照放到棧中,包括函式的區域性變數,引數和返回值。
基本型別儲存在棧中,物件、複雜型別或者引用型別儲存在堆中。
任何函式的呼叫都會在棧頂被壓入。
一旦主程序完成所有任務,堆中的物件便不會被引用而孤立。

隨著我們程式的執行,堆中的資料會越來越多,因為無人看管,棧由系統自動管理,所以無需操心。
所以,為了管理堆空間,垃圾回收機制進場了。

V8記憶體管理:GC

我們知道V8如何分配記憶體空間,接下來讓我們看看V8是如何管理堆空間的。

舉個簡單的例子,V8會釋放被孤立的物件,被孤立的物件一般指,不直接或間接被引用的物件,這樣就為新物件騰出了空間。

因此,V8垃圾回收機制代表著:回收未使用記憶體供V8進行復用。

具體的實現如下:

Minor GC (Scavenger)

清道夫GC,主要管理新生代空間,保證新生代空間的緊湊和乾淨。所有的物件都會分配到新生代空間,新生代空間相對較小。大約在1M ~ 8M,可以通過命令控制。

該GC流程大致是這樣:
新生代空間由兩個等分的空間組成,to-space和from-space,可以把這兩個名字理解為階段(當前使用當前未使用),而不是名字。當當前使用沒有更多記憶體後,觸發Minor GC。
比如,當當前使用沒有更多空間了,這時候,新增了一個待分配空間物件,Minor GC會吧當前未使用切換為當前使用,然後將之前已滿負荷的空間中的資料,根據是否存活整理到新的、空的當前使用中。存活的就放過去,非存活的就扔掉。並且在移動存活資料的時候,會緊湊擺放,避免空間浪費。最後吧待分配空間物件,分配到新的當前使用中。
當更多的待分配空間進來時,會重複上面流程,但是如果一個數據存活兩次以上,它就會被扔到老生代空間。以保證新生代空間的

上面的過程可以用一個例子來概括:
小明去網咖,第一次去,有位置,直接上機。沒位置,得等老闆清理下到時間的機器,然後再上機。這種到時間的一般是沒辦卡的(非存活)。
第二次去,還是上面的流程。
第N次去,小明也辦了張卡(存活)
如果小明的卡到期了,那麼他也就失活了。

上面所述過程,一定要參考下這個流程圖!!!

Major GC(Mark-Sweep & Mark-Compact)

該GC主要負責老生代空間的緊湊和清理。當V8知道老生代空間無更多空間的時候就會觸發這個GC。上面的清道夫GC對於體積小的資料來說是完美的,但是對於體積大的來說,非也。一種更好的方式是:標記-掃描-緊湊演算法,顧名思義,一共是三步。

不多解釋。自己看圖吧。

總結

以上介紹了記憶體的堆疊,也對比了資料結構的堆疊。
之後簡析了幾種GC方式和V8的具體實現。
更多深入的內容歡迎交流,互相學習。

參考

memory-management
V8記憶體簡析
記憶體和資料堆