4

V8 堆栈空间和垃圾回收机制

 3 years ago
source link: https://segmentfault.com/a/1190000039806340
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

V8 堆栈空间和垃圾回收机制

发布于 44 分钟前

微信公众号:[前端一锅煮]
一点技术、一点思考。

  • 栈空间
  • 堆空间
  • 新生代内存回收
  • 老生代内存回收
  • 标记清除、标记整理、增量标记

JavaScript 引擎的内存空间主要分为栈和堆。

V8 的垃圾回收策略主要基于分代式垃圾回收机制。按照对象的存活时间将内存的垃圾回收进行不同分代,然后分别对不同分代的内存使用最适合的算法。主要分为新生代和老生代,有标记清除、标记整理、增量标记等方法。

栈是临时存储空间,主要存储局部变量和函数调用。

基本类型赋值(Number, Boolean, String, Null, Undefined, Symbol, BigInt),系统会为新的变量在栈内存中分配一个新值。

引用类型赋值,系统会为新的变量在栈内存中分配一个值,这个值仅仅是指向同一个对象的引用,和原对象指向的都是堆内存中的同一个对象。

对于函数,解释器创建了”调用栈“来记录函数的调用过程。每调用一个函数,解释器就把该函数添加进调用栈,解释器会为被添加进来的函数创建一个栈帧(用来保存函数的局部变量以及执行语句)并立即执行。如果正在执行的函数还调用了其他函数,新函数会继续被添加进入调用栈。函数执行完成,对应的栈帧立即被销毁。

两种查看调用栈的方法

使用 console.trace() 向 web 控制台输出一个堆栈跟踪。

浏览器开发者工具进行断点调试。

栈虽然很轻量,在使用时创建,使用结束后销毁,但是不是可以无限增长的,被分配的调用栈空间被占满时,就会引起”栈溢出“的错误。

(function foo() {
  foo()
})()

Maximum call stack size exceeded.

为什么基本数据类型存储在栈中,引用数据类型存储在堆中?

JavaScript 引擎需要用栈来维护程序执行期间的上下文的状态,如果栈空间大了的话,所有数据都存放在栈空间里面,会影响到上下文切换的效率,进而影响整个程序的执行效率。

堆空间存储的数据比较复杂,大致可以划分为 5 个区域:

  1. 新生代内存区(new space):新生代内存区会被划分为两块,分别是 from space 和 to space(具体有什么用下文会说),64位系统下默认 32MB,32位系统下默认 16MB,通常新创建的对象会先放入 from 中。
  2. 老生代内存区(old space):较为持久的保存对象,分为两个区域 old pointer space 和 old data space 分别用来存放 GC 后还存活的指针信息和数据信息,64位系统下能使用约 1.4GB,32位系统下能使用约 0.7GB。
  3. 大对象区(large object space):这里存放体积超越其他区大小的对象,主要为了避免大对象的拷贝,使用该空间专门存储大对象。
  4. 单元区、属性单元区、Map区(Cell space、property cell space、map space):Map 空间存放对象的 Map 信息也就是隐藏类(Hiden Class)最大限制为 8MB;每个 Map 对象固定大小,为了快速定位,所以将该空间单独出来。
  5. 代码区 (code Space):主要存放代码对象,最大限制为 512MB,也是唯一拥有执行权限的内存。

新生代内存是临时分配的内存,存活时间短,老生代内存是常驻内存,存活时间长。

新生代内存回收

新生代内存中的垃圾回收主要通过 Scavenge 算法进行,具体实现时主要采用 Cheney 算法。

Cheney 将内存空间一分为二,一块叫做 From 正在使用的内存,另一块叫做 To 目前闲置的内存。

Scavenge GC算法:

  • 存活的对象从 from space 转移到 to space
  • 清空 from space
  • from space 与 to space 互换
  • 完成一次新生代 GC

简而言之,在垃圾回收的过程中,将存活对象在两个空间之间进行复制。

Scavenge 是典型的牺牲空间换取时间的算法,缺点是只能使用堆内存中的一半,这是由划分空间和复制机制所决定的。但由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上有优异的表现。

老生代内存回收

V8 在老生代中主要采 用了 Mark-Sweep 和 Mark-Compact 相结合的方式进行垃圾回收。

当一个对象经过多次复制依然存在时,它将会被认为是生命周期较长的对象,这种对象会被移到老生代中,采用新的算法进行管理,这种移动称之为“晋级”。

对象晋级的条件主要有两个:

已经经历过一次 Scavenge 回收

To(闲置内存)空间的内存不足75%

标记清除(Mark-Sweep)

标记清除,分为标记和清除两个阶段。在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。可以看出,Scavenge 中只复制活着的对象,而 Mark-Sweep 只清理死亡对象。

标记清除最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。为了解决标记清除的内存碎片问题,标记整理(Mark-Compact)被提出来。

标记整理(Mark-Compact)

Mark-Compact 是标记整理的意思,在 Mark-Sweep 的基础上演变而来。

标记整理对待未存活对象不是立即回收,而是将存活对象移动到一边,然后直接清掉端边界以外的内存。

为了避免出现 JavaScript 应用程序与垃圾回收器看到的不一致的情况,进行垃圾回收的时候,都需要将正在运行的程序停下来,等待垃圾回收执行完成之后再回复程序的执行,这种现象称为“全停顿”。如果需要回收的数据过多,那么全停顿的时候就会比较长,会影响其他程序的正常执行。

为了避免垃圾回收时间过长影响其他程序的执行,V8将标记过程分成一个个小的子标记过程,同时让垃圾回收和JavaScript应用逻辑代码交替执行,直到标记阶段完成。我们称这个过程为增量标记算法。

通俗理解,就是将原本一口气完成的标记任务分为了很多小的部分去完成, 每完成一个小任务就停一会, 让 js 逻辑执行一会, 然后再继续执行下面的部分。

从 V8 垃圾回收机制可以看到,垃圾回收是一件非常耗时的事情, 以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要 50ms 以上,做一次非增量式的垃圾回收甚至要 1s 以上,所以要做限制。

新生代设计为一个较小的内存空间是合理的,而老生代空间过大对于垃圾回收并无特别意义。V8 对内存限制的设置对于 Chrome 浏览器这种每个选项卡页面使用一个 V8 实例而言,内存的使用是绰绰有余了。对于 Node 编写的服务器端来说,内存限制也并不影响正常场景下的使用。但是对于 V8 的垃圾回收特点和js 在单线程上的执行情况,垃圾回收是影响性能的因素之一。想要高性能的执行效率,需要注意让垃圾回收尽量少地进行,尤其是全堆垃圾回收。

vue-cli 打包内存溢出,修改内存限制

node_modules/.bin

vue-cli-service

#!/usr/bin/env node --max_old_space_size=4096

调整老生代内存限制,单位mb

node --max-old-space-size=2048 build/build.js

调整新生代内存限制,单位kb

node --max-new-space-size=2048 build/build.js


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK