本文首发于公众号:符合预期的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开发人员来讲,这一级别的信息就足够了,我但愿它能帮助您编写更好的代码,考虑到这些因素,对于更高性能的应用程序,记住这些能够帮助您避免下一个可能遇到的内存泄漏问题。