V8引擎详解(六)——内存结构

6,398 阅读8分钟

前言

本文是V8引擎详解系列的第六篇,重点内容是关于V8的内存结构,以及通常情况下内存的使用过程,本文会先从基本概念入手,了解V8的堆栈结构,最后描述一个对象创建后在内存中的生命周期(本文不会有太多GC相关内容,关于垃圾回收会在下一篇详细描述)文末会有已经完成的系列文章的链接,本系列文章还在不断更新欢迎持续关注。

什么是内存

通常我们说的计算机由5个部分组成,控制器、运算器、输出设备、输出设备、存储器,而我们说的内存通常属于存储器,而程序运行时CPU需要调用的指令和数据只能通过内存获取(硬盘只有存储功能,执行时会将数据缓存到内存中),所以不管是什么语言的程序,运行时都依赖内存,而内存生命周期基本都是一致的:

  • 分配所需要的内存
  • 使用分配到的内存(读、写)
  • 不需要时将其释放\归还

而很多文章讲 javascript的内存如何如何,事实上,我认为这种说法是不准确的,本身javascript只是一种语言,真正进行内存调用分配的是javascript依赖的引擎,本文就来简单聊一下V8的内存结构。

简述堆和栈

为什么是堆和栈

在V8引擎中,可以先粗犷的分为两个部分
那栈指的就是 调用栈,首先栈的特点后进先出(普通意义上栈的特点在这里不会细说,网上相关文章很多),同时栈空间是连续的,在需要分配空间和销毁空间操作时,只需要移动下指针,所以非常适合管理函数调用。

而正因为栈空间是连续的,那它的空间就注定是非常有限的,所以不方便存放大的数据,这时我们就使用了 内存堆 来管理保存一些大数据。

基础类型和引用类型

这里要先说一下两种变量类型:基本类型变量引用变量类型,基础变量类型包括undefined, null, Number, String, Boolean, Symbol,而引用变量类型包括:Object、Array、Function等等,而实际上在js中Array、Function这些都是基于Objct的,我们可以理解引用变量类型指的就是Objct。
(这里可能有人会说 null不应该是空指针对象类型吗,typeof null === 'Object'应该算是对象,事实上这里是一个设计上的历史遗留问题,而对V8系统来说无论是null和Undefined都只是一个存在与栈里的固定的值)。

因为基础变量类型的值通常是简单的数据段,占用固定大小的空间,所以会存储在 中,而对象大小不定且通常会占用较大空间所以会存储在 中,而在栈空间会保存对象存储在堆空间的地址。

我们将一段代码通过一张图来简单看一下。

var a = 123;
var b = 'abc';
var c = {x: 1};
var d = 123;
var f = c;
var g = {x: 1};

基础类型的值在创建时会开辟一块内存空间,将内存地址存储在对应的变量上,如果此时再创建一个基础类型等同于之前创建过的值,会直接将地址存储在新创建的变量上,所以就会有 a === d

那么如果创建一个对象,就会在堆中开辟一块空间用来存储对象,将内存地址存储在对应的变量上,如果此时创建一个新的变量(f)赋值为之前所创建的存储对象地址的变量(c),那么会将c存储的堆内存地址赋值给f,就会有 c === f

如果此时再创建一个新的对象变量g,就会在堆中再开辟一块空间来创建对象,将地址赋予g,但是即使对象内容一样,地址不同指向的也是两块空间,就会有 g !== c

关于函数调用也很好理解,也是用一段代码一张图来表示如下:

function main() {
    func1();
}
function func1() {
    func2();
    func3();
};
function func2() {};
function func3() {};
main();

在函数间的嵌套调用的过程中外层的函数不会释放,而栈的空间是有限的也有着严格的数量限制,所以在使用递归的时候要注意是否会溢出

V8内存管理的核心——堆

栈的管理通常比较容易一点,通过上下移动指针来管理即可,而堆的管理相对复杂很多,而我们通常说的垃圾回收等也主要针对堆来说的。

堆空间的结构

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

(图片来源:www.imooc.com/article/300…
V8引擎初始化内存空间主要将堆内存分为以下几个区域:

  • 新生代内存区(new space)
    新生代内存区会被划分为两个semispace,每个semispace大小默认为16MB也就是说新生代内存区通常只有32MB大小(64位),而这两个semispace分别是from space 和 to space(具体有什么用下文会说),通常新创建的对象会先放入这两个semispace中的一个。

  • 老生代内存区(old space)
    通常会较为持久的保存对象,也分为两个区域 old pointer spaceold data space分别用来存放GC后还存活的指针信息和数据信息。

  • 大对象区(large object space)
    这里存放体积超越其他区大小的对象,主要为了避免大对象的拷贝,使用该空间专门存储大对象。

  • 单元区、属性单元区、Map区(Cell space、property cell space、map space)
    Map空间存放对象的Map信息也就是隐藏类(Hiden Class)最大限制为8MB;每个Map对象固定大小,为了快速定位,所以将该空间单独出来。

  • 代码区 (code Space)
    主要存放代码对象,最大限制为512MB,也是唯一拥有执行权限的内存

内存运行的生命周期

堆内存空间分成了有不同功能作用的空间区域,大对象区,map区,代码区没什么好说的,重点还是了解一下新生代内存区老生代内存区

这里我们假设创建了一个对象 obj,先说一下新生代内存区的两个space也就是 from spaceto space 的作用。

  • 首先obj会被分配到 新生代 中两个space中其中一个space,这里我们假设分配到了from space中。
  • 程序继续执行会不断的向from space中添加新的对象信息,这时from space将要达到了存储的上限(16MB),V8的垃圾回收机制会开始清理from中不再被使用的对象(即没有被指向的对象)。
  • 清理后,将所有仍然存活的对象(我们假设obj还存活),会被复制到to space然后删除所有from space中的对象。
  • 这时,程序继续运行,如果有新创建的对象会不断的分配到to space中,当to space快要满了重复执行上面说的复制转移的工作。

也就是说创建的对象会在to space 和 from space 之间转移,也就是所谓的 to --> from, from --> to 的角色互换过程。

(本文重点不是垃圾回收,所以很多GC相关的内容不会很详细)

接下来说一下老生代内存区,现在继续看上文说的那个对象 obj:

  • 经过程序一段时间运行后的obj依然存活在新生代内存区,终于满足了晋升的条件,便转移到了老生代内存区。
  • 又过了一段时间对象 obj 终于不被引用了,同时老生代内存区域空间也被占用了很多的空间,V8就会在老生代里面进行遍历,发现了对象 obj 已经不被引用了,于是给他打了个标记。
  • 由于V8是单线程的执行机制,V8为了避免一次清除占用太多时间,会给这批打了标记的待清理对象进行分批回收,至此这个对象就在内存中释放掉了。

总结

本文主要学习了一些内存的概念以及V8的内存结构,以及各部分的一些作用,事实上无论是面试还是日常工作中,理解V8的内存机制会对我们带来很大帮助,那在下一篇我会重点说一下V8的内存回收机制。如果有什么错误,请在评论中和作者一起讨论,如果您觉得本文对您有帮助请帮忙点个赞,感激不尽。

参考文章

www.imooc.com/article/300…

系列文章

V8引擎详解(一)——概述
V8引擎详解(二)——AST
V8引擎详解(三)——从字节码看V8的演变
V8引擎详解(四)——字节码是如何执行的
V8引擎详解(五)——内联缓存
V8引擎详解(六)——内存结构
V8引擎详解(七)——垃圾回收机制
V8引擎详解(八)——消息队列
V8引擎详解(九)——协程&生成器函数