揭秘 node 中的内存管理及垃圾回收机制 😜

255 阅读15分钟

前言

今天是 6月更文的第n天。

闲来无事摸会儿鱼🤣,这不恰巧看到了一位大佬面阿里的题,在面试中问到了垃圾回收的相关问题。

不就是垃圾回收嘛平时也没少见,这么一道平平无奇的面试题怎么就引起了我的兴趣呢?

那当然是大佬回答啦,刷题的人肯定只知道标记清理,但是却从未想过深入 js 引擎底层的了解其内存的分配原理

那就借此机会揭秘一下 V8 引擎中的 新生代 和 老生代吧 😉

什么是 js 引擎?

在现有的 javascript 引擎中,V8引擎绝对是其中的佼佼者,chrome 和 node 底层都使用了 V8 引擎,其中 chrome的市场占有率达到 70%,而 node 更是前端工程化以及扩展边界的核心支柱,V8 引擎对于一个前端开发工程师来说重要程度可想而知。

先来了解一下什么是 js 引擎

JavaScript 本质上是一种解释型语言,与编译型语言不同的是它需要一遍执行一边解析, JavaScript 引擎是一种解释和执行 JavaScript 代码的软件组件或程序。它负责将 js 代码转换为可执行的机器码或字节码,并执行该代码以实现预期的功能。

js 引擎通常作为浏览器或 js 运行时环境的一部分,用于解析、编译和执行 js 代码。

JavaScript 引擎的主要功能:

  • 词法和语法解析:JavaScript 引擎负责解析 JavaScript 代码,将其分解为AST语法树和词法单元。通过识别和解析代码中的关键字、表达式、语句等,构建出代码的语法结构。
  • 优化和编译:JavaScript引 擎通常会对解析后的代码进行优化和编译,以提高执行效率。优化技术包括内联缓存、即时编译(JIT)等,可以根据代码的执行情况和上下文信息进行动态优化。
  • 内存管理:JavaScript 引擎同时会负责管理 js 代码使用的内存。它会分配和释放内存资源,进行垃圾回收来清理不再使用的对象,并优化内存的分配和使用方式。
  • 执行环境:JavaScript 引擎创建和管理 JavaScript 代码的执行环境。它负责创建全局对象、函数作用域、执行上下文等,并提供变量、函数和对象的执行环境。

常见的 JavaScript 引擎包括:

  • V8引擎:由 Google 开发,用于 Google Chrome 浏览器和 Node.js 运行时环境。
  • SpiderMonkey 引擎:由 Mozilla 开发,用于 Firefox 浏览器。
  • JavaScriptCore 引擎:由 WebKit 团队开发,用于 Safari 浏览器。
  • Chakra 引擎:由微软开发,用于 Internet Explorer 浏览器和 Microsoft Edge 浏览器。

V8 引擎

V8 引擎是一种开源的 JavaScript 引擎,由 Google 开发并用于 Google Chrome 浏览器中。它主要用于解释和执行 JavaScript 代码,并将其转换为计算机可以理解和执行的机器码。

js 作为一款解释型语言,为了提高性能,在 V8 引擎中引入了 Java 虚拟机和 C++ 编译器中的众多技术。

V8 将其编译成原生机器码(IA-32, x86-64, ARM, or MIPS CPUs),并且使用了如内联缓存(inline caching)等方法来提高性能。

V8 引擎高性能、单线程的特点从以下几点体现:

  • V8 引擎在执行 JavaScript 代码时采用了即时编译技术提高了编译性能,将 JavaScript 代码转换为机器码,并进行优化,提高执行速度。
  • V8 引擎按照代码的顺序执行 JavaScript 任务,并处理事件循环机制,确保任务按照正确的顺序执行。
  • V8 引擎负责分配和管理 JavaScript 对象的内存。它使用了分代垃圾回收机制,通过自动检测不再使用的对象,并进行垃圾回收,释放内存空间。
  • 支持 ECMAScript 标准:V8 引擎遵循 ECMAScript 标准,支持最新的 JavaScript 特性和语法。它不仅支持基本的 JavaScript 语法,还支持箭头函数、模板字符串、解构赋值等现代化的语言特性。

V8 引擎除了在 Chrome 浏览器中使用外,还被许多其他项目广泛采用,包括 Node.js、Electron、MongoDB 等。它的高性能和强大的功能使得 JavaScript 在前端开发、服务器端开发和嵌入式应用开发等领域得到广泛应用。

概念

还不了解垃圾回收、内存泄露的同学先在这一章节补充一下必备的知识点,以便后续内容的理解 !!!

垃圾回收

垃圾回收(Garbage Collection)是一种自动化的内存管理机制,用于检测和回收不再使用的内存对象,以释放内存空间。

在编程语言中,包括 JavaScript 和其它一些高级语言,垃圾回收机制的触发时机是由运行时环境自动决定的,开发者通常无需显式地触发垃圾回收。

在 JavaScript 中,具体的垃圾回收时机是由 JavaScript 引擎(如 V8 引擎)负责控制的。一般来说,垃圾回收机制会在以下情况下触发:

  1. JavaScript 引擎会根据内存使用情况,设置一个阈值来决定何时触发垃圾回收。当内存占用超过阈值时,垃圾回收机制会启动,清理不再使用的对象。
  2. JavaScript 引擎会周期性地运行垃圾回收,检查和回收不再使用的对象,避免内存过度占用,确保应用程序的性能和稳定性。
  3. 垃圾回收机制通过检测对象是否可达来判断是否将其回收。如果一个对象不再被任何引用所指向,无法通过任何路径访问到该对象,那么垃圾回收机制会将其标记,并在后续的垃圾回收过程中将其回收。

具体的垃圾回收算法和时机在不同的 JavaScript 引擎中会有所不同。

不同的引擎可能采用不同的垃圾回收策略,在开发过程中无需手动干预垃圾回收过程,但可以通过编写高效的代码和避免内存泄漏等问题,来优化应用程序的内存使用。

内存泄漏

内存泄漏(Memory Leak)是指程序中存在一些不再使用的内存对象,但这些对象仍然被占用而无法被垃圾回收机制释放,导致内存的持续增加,最终可能会使应用程序的性能下降甚至导致内存耗尽。

可能会导致内存泄露的场景:

  1. 当一个对象不再需要时,仍然存在对该对象的引用,就会导致内存泄漏。这通常发生在使用全局变量、缓存对象、事件监听器等场景下。如果不及时释放对对象的引用,垃圾回收机制就无法回收这些对象。
  2. 在对象之间的循环引用导致这些对象无法被垃圾回收机制释放。例如,当对象A引用了对象B,而对象B又引用了对象A,因此形成了循环引用。如果没有及时打破循环引用,就无法回收这些对象。
  3. 在使用定时器或回调函数时,没有及时的清理,未清除的定时器或回调函数会持续存在,从而导致内存泄漏的发生。
  4. 在使用 JavaScript 操作 DOM 元素时,没有正确地释放对 DOM 元素的引用,也会导致内存泄漏。例如,未清除的事件监听器、未移除的 DOM 元素等都会占用内存。
  5. 如果应用程序中积累了大量的数据,而这些数据不再使用但仍然存在于内存中,这可能是因为缓存、历史记录等导致的内存泄漏。

内存泄漏会逐渐消耗可用内存,最终导致应用程序的性能下降甚至崩溃。为避免内存泄漏,开发者应注意正确管理对象的生命周期,及时释放不再需要的对象和资源,避免无意的引用和循环引用,以及正确处理定时器、回调函数和 DOM 引用等。

有了这些知识储备我们就要开始步入正题啦 😉

V8 中的内存结构

既然 V8 引擎会对内存进行管理,那其中的内存结构到底是什么样的呢?

什么是内存?

学过计组的同学肯定会知道(这么多年过去了我是全还给老师了🤣)

计算机由5个部分组成:控制器、运算器、输出设备、输出设备、存储器

而内存就属于存储器,在计算机系统中用于存储数据和程序的硬件设备,是计算机系统的重要组成部分,它对于程序的执行和数据处理起着关键的作用,用于临时存储和访问数据,包括程序的指令和运行时数据。

内存是由一系列存储单元组成,每个存储单元都有一个唯一的地址。这些存储单元可以存储和检索数据,每个单元可以存储固定大小的数据(通常是字节)。内存中的每个单元都可以通过其地址进行访问,使得计算机可以快速读取和写入数据。

栈 & 堆

众所周知内存分为 栈内存 和 堆内存,

每个 V8 进程有一个栈。在栈中主要存储静态数据,包括方法/函数框架、原语值和指向对象的指针。

正如我们常见的基础类型就存放在栈内存中,

引用数据类型会在栈内存中存储数据的地址,而该地址则指向对象或数组则是保存在堆内存中。而复杂类型的数据则存储在堆中。

那堆内存是什么样的呢?

我们先来看一下内存的组成结构:

(图片来源于www.imooc.com/article/300…

如上图所示堆内存这是 V8 存储对象和动态数据的地方,同时也是内存中区域中最大的块,也是垃圾回收发生的地方。

在现代的JavaScript引擎中,内存是被划分为不同的区域,且每个区域都有各自不同的垃圾回收策略。

常见的内存区域包括新生代、老生代和大对象区域。

  1. 新生代(New Generation):主要存储临时对象和新对象的地方,大部分对象的生命周期都很短。新生代中又被分为两个区域( semispace ):分别为 From 空间和 To 空间。当一个对象被创建时,它首先被分配到 From 空间中。在新生代中采用一种快速的垃圾回收算法 Scavenge(清扫)。

  2. 老生代(Old Generation):存储了存活时间较长的对象或将常驻内存。在经过多次的新生代回收后,仍然存活的对象将被晋升到老生代。对于老生代的垃圾回收,通常使用标记-清除(Mark-Sweep)和标记-整理(Mark-Compact)等算法

  1. 大对象区域(Large Object Space):大对象区域用于存储体积较大的对象。这些对象通常比较稳定,不容易被释放或移动。因此,对于大对象的垃圾回收采用的是分代式垃圾回收的策略,但具体实现会因引擎而异。
  1. 代码空间(Code-space)

这就是即时(JIT)编译器存储编译代码块的地方。这是唯一有可执行内存的空间(尽管代码可能被分配在“大对象空间”中,它们也是可执行的)。

  1. 单元空间、属性单元空间、映射空间(Cell space, property cell space, and map space)

这些空间分别包含Cell,PropertyCell 和 Map. 这些空间中的每一个都包含相同大小的对象,并且对它们指向的对象类型有一些限制,这简化了收集。

新生代

在新生代中它的空间被一分为二,在这两个 semispace 中,只有一个处于使用中,而另一个会处于闲置中。处于使用状态的空间称为 From 空间,处于闲置状态的空间称为 To 空间。

新生代中的垃圾回收机制了解一下:

  1. 当一个对象被创建时,它首先被分配到 From 空间中。

  2. 垃圾回收器会定期进行扫描,检查 From 空间中的存活对象

  3. 将仍然存活的对象复制到 To 空间,并释放 From 空间中不再使用的对象。

  4. 完成复制后,再将 From 和 To 空间交换角色,使得对象仍存活在 From 空间中

在新生代垃圾回收过程中,就是通过将存活对象在两个空间之间进行复制的行为,而这个过程被称为 Scavenge(清扫),它是一种快速的垃圾回收算法。因为 To 的这一半空间是被浪费的,它在垃圾回收过程中没有被利用到。

Scavenge 算法的优势在于它采用了复制的方式来回收内存,避免了复杂的标记和整理过程,因此回收速度较快。同时,由于只需要处理一半的内存空间,所以垃圾回收的停顿时间也相对较短。

这种空间换时间的权衡是为了在新生代的垃圾回收中提供更好的性能。

如果新生代中的对象一直存活不被清除又会发生什么事情呢?

对象晋升

当一个对象经过多次复制仍然存活,它将会被认为是生命周期较长的对象,随后这种对象会被移动到老生代堆内存中,采用新的算法进行处理,从新生代移动到老生代的过程就被称为对象晋升。

From 空间中的对象在复制到 To 空间时会进行检查。若满足一定条件,则会将该对象移动到老生代中,完成对象晋升。

需要满足对象晋升的条件有以下两个:

  1. 对象是否经历过一次 Scavenge 算法
  2. To 空间的内存占比是否超过25%

当对象晋升成功后,将会在老生代中被作为存活周期较长的对象来对待,并通过新的算法处理回收。

老生代垃圾回收

当新生代的对象历经千辛万苦晋升为老生代后,这里存在着大量的存活对象,原有的 Scavenge 算法在这里将不再适用,对于老生代的垃圾回收,通常使用标记-清除(Mark-Sweep)和标记-整理(Mark-Compact)等算法。

标记-清除算法首先标记所有活动对象,然后清除未被标记的对象。

标记-整理算法除了标记和清除,还会对活动对象进行整理,使它们在内存中连续排列,从而减少内存碎片化。

Mark-Sweep(标记清除)分为标记和清除两个阶段,具体步骤如下:

  1. 从根对象开始遍历,标记所有被引用的对象
  2. 将可达的对象标记为活动对象,而未被标记则为垃圾对象
  3. 在标记完成后,开始清除所有未被标记的对象

Mark-Sweep 算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收。

而在标记清除过程中可能会产生内存碎片,造成可用空间不连续的情况。这种内存碎片会对后续的内存分配造成问题,为了解决内存碎片化的问题,Mark-Compact(标记整理)被提出

Mark-Compact(标记整理)分为标记-清除-整理三个阶段,具体步骤如下:

在完成上面的标记清除后,会对内存空间进行整理,通过将活动对象整理到一侧,产生连续的可用空间,提高内存利用率。

然而,相比于 Mark-Sweep 算法,Mark-Compact 算法需要额外的复制和更新引用的步骤,可能会增加垃圾回收的时间开销。所以V8主要使用 Mark-Sweep,只有当内存空间不足以对晋升对象进行分配时才使用 Mark-Compact。

关于三种不同的立即回收算法

总结

本文介绍了 V8 引擎的的相关内容,更加深入的了解了其中的内存组成结构,以及新生代老生代对应的垃圾回收机制,虽然 Node.js 中会自动触发垃圾回收,但仍然存在内存泄漏的情况。所以在日常开发的过程中我们需要注意及时释放不再使用的对象和资源,例如定时器、绑定的事件监听、以及获取的DOM 元素,避免引发内存泄漏的问题。

参考

  1. V8引擎的内存管理
  2. V8引擎详解 系列
  3. V8引擎垃圾回收原理解析