V8 分配内存和回收数据的全链路详解

常驻内存集
程序在运行时,都给程序会分配内存,让程序能保持运行,Javascript 程序在运行时 V8 也会给它分配内存,这种内存可以叫做常驻内存集合
,V8 给这种常驻内存进一步细分成栈
和堆
,简单的分类可以看下图:
来分析一下栈
和堆
分别是什么
栈(stack)
栈
的特性是后进先出,用于存储 javascript 中的基本类型和引用类型的指针,每个 V8 进程都会有一个栈区域,它的存储顺序是连续的,所以在新增或删除数据是也只需要将指针移动到对应的位置,然后删除或修改数据,所以栈的速度非常快。
例如在 javascript 声明基本类型变量,他会在栈中这样表现
1 | var num = 1; |
堆(heap)
堆是 V8 内存分配一个重要的组成部分,主要用于存储 js 中的引用类型
。看一下V8 源码中初始化 heap 的代码
1 | // src/heap/heap.cc |
从代码中可以看到,heap 分成了 7 个部分。相应地,在 node 的v8
模块中可以查看 v8 给 javascript 程序分配堆内存的大小
1 | const v8 = require('v8'); |
执行后返回
1 | [ |
调用v8.getHeapSpaceStatistics()
可以看到每个堆空间分配的大小和可用大小。
接下来详细介绍一下这里面每一个堆空间代表着什么
新生代(new space)
新生代内存用于存放一些生命周期比较短的对象数据,例如:一些新创建的对象数据会存放到 new space 中。
新生代内存有两个区域,分别是对象区域(from) 和 **空闲区域(to)**,两个区域都用一个SemiSpace
对象来管理
新生代内存使用Scavenger
算法来管理内存。
SemiSpace
SemiSpace
对象主要负责管理地址,不负责垃圾回收和内存分配。
SemiSpace
类的部分源码
1 | // src/heap/new-spaces.h |
SetUp
方法负责设置管理地址范围
1 | void SemiSpace::SetUp(size_t initial_capacity, size_t maximum_capacity) { |
TearDown
负责重置管理的地址
1 | void SemiSpace::TearDown() { |
new space
内存分配规则
new space 的内存分配非常简单,规则如下:当需要分配内存时,new space 有一个分配指针指向需要分配的内存值,分配完成后指向下一个指针地址,直到空间被分配满,这时就会触发Scavenger
算法进行垃圾回收。Scavenger(Minor GC)
算法
Scavenger
用于new space
的垃圾收集,回收的规则如下:当前 new space 空间如下:
空间 1,2 仍在使用,3,4 则不再使用,这时有一个新的数据5
需要存放当
from
空间被分配满后,有数据新的数据需要存放,触发垃圾回收机制,将仍在使用的数据复制到to
中,不需要使用的则销毁,新数据存放到to中
。如下图:当
to
空间分配满后,空间 1,2 仍在使用,空间 5 和其他数据不再使用。若有数据进入会触发数据 GC 机制,空间 1,2 会进入老生代,而不再使用的数据则会被清除掉。如下图
new space
就在这种“反复横跳”的过程中进行内存管理,这种内存管理快速而且高效,虽然 new space 分配到的内存不多,但它的速度很快。同时也会造成很大的空间浪费,因为from
和to
总是有一个是会置空的。
老生代(old space)
old space
其实相当于常驻内存,v8 会将长期停留在new space
的数据放到old space
中。
Oldspace 类的代码如下:
1 | // src/heap/paged-spaces.h |
可以看到它继承了PagedSpace
类,PagedSpace
类用于创建和管理 space,除了OldSpace
,继承PagedSpace
类的还有CodeSpace和MapSpace。
- 常驻内存分配
上文提到:v8会将长期停留在new space的数据放到old space中
。那么它具体的规则是:当new space
的 Scavenger 进行两个周期的垃圾回收后,如果数据还存在new space
中,则将他们存放到old space
中。
old space
又可以分为两部分,分别是Old pointer space
和Old data space
Old pointer space
主要存放指向其他对象的指针。Old data space
主要存放数据对象
Old Space 使用Major GC(Mark-Sweep & Mark-Compact)
进行内存管理
- Major GC(Mark-Sweep & Mark-Compact)
Major GC
主要用于Old Space
内存管理,它的触发条件是:当没有足够的Old Space
进行分配时,他就会触发垃圾回收。
Major GC
使用的是Mark-Sweep-Compact(标记-清除-整理) 算法进行垃圾回收,所以 Major GC 进行垃圾回收主要有三步。- 标记
首先,垃圾回收算法会判断哪些数据仍在使用,哪些数据不再使用。垃圾回收中会有一个root
节点记录那些节点仍在使用,用按深度优先
的方法遍历节点树,如果数据不在树中,则会被标记为不再使用。 - 清除
回收算法把不再使用的对象标记出来后,将这些空间重新标记为空,使他们能够重新存放其他对象 - 整理
当清除
的动作执行完后,这时内存空间可能会出现比较碎片化的问题,这不利于新进入的对象分配,这时需要整理一下内存空间,提高给对象分配内存的速度。
- 标记
使用这种垃圾回收方法进行回收时,会将所有正在运行的程序停止,等待垃圾回收完成后再恢复,所以它会阻塞jsvascript 程序运行。
为了避免这种回收的时间过长影响 js 执行,v8 引擎在这方面做了这些:
- 将标记-清除-整理分成了多个步骤进行
标记
会放在辅助线程中进行,从而不阻塞 Javascript 主线程,当 Javascript 创建对象同时,辅助线程也会将它标记起来。清除
和整理
会在辅助线程中完成,从而不阻塞 Javascript 主线程- 懒清除:垃圾回收的执行会在需要使用内存时才会执行
其他空间介绍
- Code space 用于存放 JIT 已编译的代码
- Large object space 一个大于其他空间的对象,每个对象都有一个独立的内存。垃圾回收不会动这里的对象
- Map space 只存放 map 对象,而且他们不会移动。
分配内存
简单地了解完 V8 中的栈和堆。下面看一下 javascript 中声明变量后在 v8 时如何分配的
基本类型
基本类型会直接存放到栈中,例如上文例子:
1 | var num = 1; |
引用类型
引用类型包括 function
,array
,object
。他们在声明后,栈的值时 heap 的指针值。来看一个例子
1 | function Test(num, people, bool) { |
内存分配的图如下:
实例化数据后内存分配如下
执行完实例化语句后
小结
本文主要介绍了 V8 的内存分配和数据回收,主要内容:
- 常驻内存集,了解栈和堆分别时什么
- 堆(heap)分配的空间和垃圾回收算法
- Javascript 声明变量后栈和堆是如何分配内存的