本文首發於公衆號:符合預期的CoyPan
這是一篇譯文,有部分刪減原文地址:https://deepu.tech/memory-man...javascript
原文標題:Visualizing memory management in V8 Engine (JavaScript, NodeJS, Deno, WebAssembly)java
在本章中,咱們將介紹用於ECMAScript和WebAssembly的V8引擎的內存管理,這些引擎用於NodeJS、Deno&Electron等運行時,以及Chrome、Chromium、Brave、Opera和Microsoft Edge等web瀏覽器。因爲JavaScript是一種解釋性語言,它須要一個引擎來解釋和執行代碼。V8引擎解釋JavaScript並將其編譯爲機器代碼。V8是用C++編寫的,能夠嵌入任何C++應用程序中。web
首先,咱們來看看V8引擎的內存結構。因爲JavaScript是單線程語言,因此V8爲每個JavaScript上下文使用一個進程。若是你使用service worker,V8會爲每一個service worker開啓一個新的進程。在V8進程中,一個正在運行的程序老是由一些分配的內存來表示,這稱爲常駐集(Resident Set)。能夠進一步劃分如下不一樣的部分:算法
這和咱們在上一篇文章中提到的JVM有些類似。咱們來看一看每個部分都是作什麼的:windows
這是V8存儲對象和動態數據的地方。這是內存中區域中最大的塊,也是垃圾回收(GC)發生的地方。整個堆內存不是垃圾回收的,只有新舊空間(New space、Old space)是垃圾回收管理的。堆內存能夠進一步劃分爲如下幾部分:api
新空間(或者說叫:新生代),是存儲新對象的地方,而且大部分對象的聲明週期都很短。這個空間很小,有兩個半空間,相似於JVM中的S0,S1。這片空間是由Scavenger(Minor GC)來管理的,稍後會介紹。新生代空間的大小能夠由--min_semi_space_size
(初始值) 和 --max_semi_space_size
(最大值) 兩個V8標誌來控制。數組
老空間(或者說叫:老生代),存儲的是在新生代空間中通過了兩次Minor GC後存活下來的數據。這片空間是由Major GC(Mark-Sweep & Mark-Compact)」管理的,稍後會介紹。老生代空間的大小能夠--initial_old_space_size
(初始值) and --max_old_space_size
(最大值) 兩個V8標誌來控制。這片空間被分紅了兩個部分:瀏覽器
這是大於其餘空間大小限制的對象存儲的地方。每一個對象都有本身的內存區域。大對象是不會被垃圾回收的。併發
這就是即時(JIT)編譯器存儲編譯代碼塊的地方。這是惟一有可執行內存的空間(儘管代碼可能被分配在「大對象空間」中,它們也是可執行的)。框架
這些空間分別包含Cell,PropertyCell 和 Map. 這些空間中的每個都包含相同大小的對象,而且對它們指向的對象類型有一些限制,這簡化了收集。
每一個空間都由一組頁組成。頁是使用 mmap從操做系統分配的連續內存塊。每頁大小爲1MB,但大對象空間較大。
這是棧內存區域,每一個V8進程有一個棧。這裏存儲靜態數據,包括方法/函數框架、原語值和指向對象的指針。棧內存限制可使用--stack_size V8標誌設置。
既然咱們已經清楚了內存是如何組織的,讓咱們看看在執行程序時如何使用其中最重要的部分。
讓咱們使用下面的JavaScript程序,代碼沒有針對正確性進行優化,所以忽略了沒必要要的中間變量等問題,重點是可視化棧和堆內存的使用狀況。
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);
能夠經過下面的ppt看一下在上面的代碼執行的過程當中,棧內存和堆內存是如何使用的。
如你所見:
如你所見,棧是由操做系統自動管理的,而不是V8。所以,咱們沒必要太擔憂棧。另外一方面,堆並非由操做系統自動管理的,由於堆是最大的內存空間,並保存動態數據,它可能會隨着時間的推移呈指數增加,致使咱們的程序內存耗盡。隨着時間的推移,它也變得支離破碎,減慢了應用程序的速度。這就是爲何須要垃圾回收。
區分堆上的指針和數據對於垃圾收集很重要,V8使用「標記指針」方法來實現這一點。在這種方法中,它在每一個單詞的末尾保留一個位,以指示它是指針仍是數據。這種方法須要有限的編譯器支持,但實現起來很簡單,同時效率也至關高。
如今咱們知道了V8如何分配內存,讓咱們看看它如何自動管理堆內存,這對應用程序的性能很是重要。當一個程序試圖在堆上分配比自由可用的更多的內存(取決於V8標誌集)時,咱們會遇到內存不足的錯誤。錯誤管理的堆也可能致使內存泄漏。
V8經過垃圾收集來管理堆內存。簡單地說,它釋放孤立對象(即再也不直接或間接從堆棧中引用的對象(經過另外一個對象中的引用)使用的內存,以便爲建立新對象騰出空間。
Orinoco是V8 GC項目的代碼名,用於使用並行、增量和併發的垃圾回收技術來釋放主線程。
V8中的垃圾回收器負責回收未使用的內存,供V8進程重用。
V8垃圾回收器是分代的(堆中的對象按其年齡分組並在不一樣階段清除)。V8有兩個階段和三種不一樣的垃圾收集算法:
這種類型的GC保持新生代空間的緊湊和清潔。對象被分配到至關小的空間(1到8MB之間,取決於行爲啓發)。新生代空間的分配成本很低:有一個分配指針,每當咱們想爲新對象保留空間時,它都會遞增。當分配指針到達新生代空間的末尾時,將觸發次Minor GC。這個過程被稱爲Scavenger,實現了「切尼算法」。Minor GC常常出現並使用並行的輔助線程,並且速度很是快。
讓咱們來看一看Minor GC的過程:
新生代空間被分紅兩個大小相等的半空間:from-space和to-space。大多數分配都是在to-space中進行的(除了某些類型的對象,例如老是在老生代空間中分配的可執行代碼)。當to-space填滿時,將觸發Minor GC。完成過程以下:
咱們看到了Minor GC如何重新生代內存空間那裏回收空間並使其保持緊湊的。這個過程雖然會中止其餘操做,可是這個過程是十分迅速而有效的,大部分時候都微不足道。因爲此進程不掃描老生代空間中的對象以獲取新生代空間中的任何引用,所以它使用從老生代空間到新生代空間的全部指針的寄存器。這將由一個名爲write barriers的進程記錄到存儲緩衝區。
這種類型的GC保持了老生代空間的緊湊和乾淨。當V8根據動態計算的限制肯定沒有足夠的老生代空間時,就會觸發此操做,由於它是從Minor GC週期中填充的。
Scavenger算法很是適合於較小的數據量,但對於較大的老生代空間來講是不實際的,由於它有內存開銷,所以主要的GC是使用Mark-Sweep-Compact算法完成的。它使用三色(白灰黑)標記系統。所以,Major GC是一個三步過程,第三步是根據分段啓發執行的。
這種類型的GC也稱爲stop-the-world GC,由於它們在執行GC的過程當中引入了暫停時間。爲了不這個V8使用了以下技術:
讓咱們來看一下 major GC的過程:
本文將爲您提供V8內存結構和內存管理的概述。這裏沒有作到面面俱到的,還有不少更高級的概念,您能夠從v8.dev中瞭解它們。可是對於大多數JS/WebAssembly開發人員來講,這一級別的信息就足夠了,我但願它能幫助您編寫更好的代碼,考慮到這些因素,對於更高性能的應用程序,記住這些能夠幫助您避免下一個可能遇到的內存泄漏問題。