Skip to content

V8内存管理 #26

@isNeilLin

Description

@isNeilLin

现在主流的内存管理方式可以归纳为以下几种

手动内存管理

语言本身不进行内存管理,开发者需要手动进行内存管理,例如 c,c++

垃圾回收 ♻️(GC)

垃圾回收是指自动进行内存分配以及回收操作。垃圾回收是现代语言中最常见的内存管理机制,一般以固定的时间间隔来运行该过程,垃圾回收本身会产生较小的开销,称之为暂停时间

JVM(Java/Scala/Groovy/Kotlin), JavaScript, C#, Golang, OCaml, and Ruby are some of the languages that use Garbage collection for memory management by default.

Mark & Sweep GC (标记&扫描内存回收)

也称之为追踪 GC,通常包含两步操作

  • 首先标记使用中的内存为alive
  • 其次清除非alive的内存

在 JVM 中,有多种不同的 GC 算法可供选择,而像 V8 这样的 JavaScript 引擎则使用 Mark&Sweep GC 和 Reference count GC 来对其进行补充

引用计数

在这种方法中,每个对象都获得一个引用计数,该引用计数随对它的引用的更改而增加或减少,并且当计数变为零时将进行垃圾回收。

该方式并非最优选择,因为不能解决循环引用计数的问题

在初始化获取资源 RAII

内存跟对象的生命周期绑定到一起,在构造时申请,在析构时释放。

例如 C++的 class

自动引用计数(ARC)

跟引用计数类似,但会在编译时插入对应的retainrelease指令。该方式也不能解决循环引用计数的问题,需要开发者进行规避。

v8 内存管理

V8 memory structure

由于 js 是单线程的,所以 v8 在每个 js 的上下文中使用一个进程,因此如果你使用了 Service Worker,v8 将为每一个 Service Worker 生成一个新的 v8 进程。

在 V8 进程中,始终由分配的内存来代表正在运行的程序,这称为 Resident Set。

堆内存

V8 在此处存储对象或动态数据。这是最大的内存区域,这是垃圾回收(GC)发生的地方。

垃圾回收并不是发生在整个堆内存上,只有年轻代跟老年代由垃圾回收管理。

  • 新空间(年轻代)
    新对象暂存的地方,空间很小,又被分为了两个半空间(semi-space),该空间由“ Scavenger(Minor GC)”管理。

    可以使用--min_semi_space_size(Initial)和--max_semi_space_size(Max)V8 标志来控制新空间的大小。

  • 老空间(旧世代)
    在新空间存在了两次 GC 周期的对象将被移动到 old space。该空间有 主要 GC 管理

    可以使用--initial_old_space_size(Initial)和--max_old_space_size(Max)V8 标志来控制旧空间的大小。

    • 旧指针空间:包含具有指向其他对象的指针的幸存对象。

    • 旧数据空间:包含仅包含数据的对象(无指向其他对象的指针)。

      在“新空间”中存活了两个较小的 GC 周期后,字符串,装箱的数字和未装箱的双精度数组将移到此处。

  • 大对象空间
    这里是大于其他空间大小限制的对象所在的地方。每个对象都有自己的内存区域。

    大对象永远不会被垃圾回收器移动

  • 代码空间
    即时(JIT)编译器在此处存储已编译的代码块。这是唯一具有可执行内存的空间(尽管 Codes 可以在“大对象空间”中分配,并且它们也是可执行的)。

  • Cell space, property cell space, and map space:
    这些空间分别包含了 Cells, PropertyCells, 和 Maps,
    这些空间包含了大小相同的对象,并且对它们指向的对象有一些限制,从而简化了收集。

这些空间中的每一个都由一组页面组成。页面是操作系统通过 mmap 分配的连续的内存块。除较大的对象空间外,每个页面的大小均为 1MB

Stack

每个 v8 进程都存在一个栈,用来存储静态数据。包含方法或者函数的 frame,原始类型的数据,指向对象的指针。

可以使用--stack_size V8 标志设置堆栈内存限制

v8 内存使用(Heap vs Stack)

  • 全局范围保存在 Stack 的 全局 frame 中
  • 每一次函数调用产生的 frame-block 都被 push 到 Stack 中
  • 函数 frame 中的所有局部变量,函数参数,以及返回值都被存放在 function frame block 中
  • 所有的原始类型数据都被直接存放在 stack 中。(int, string 等)
  • 所有的对象类型被存放在 heap 上,通过 stack 上的指针指向这些对象类型

    函数也是对象类型,所以函数的定义也被存放在 heap 上。

  • 当前函数的调用的新新函数,将被 push 到 stack 的顶部
  • 函数调用结束之后将会从 stack pop 出去
  • 当主进程结束之后,将不再有 stack 的指针指向 heap 中的对象,heap 中的对象全部变成孤儿对象
  • 除非你明确的复制一个对象,所有对于对象的引用都是基于指针的

stack 上的内存都是由系统自动管理的,通常情况下我们不需要担心 stack 上的内存管理。
与之相对的,heap 的上内存则不由系统管理,因此垃圾回收是针对 heap 上的内存进行管理。

区分堆上的指针和数据对于垃圾回收很重要,V8 为此使用“标记指针”方法-在这种方法中,它在每个单词的末尾保留一点以指示它是指针还是数据。

v8 垃圾回收机制

当程序尝试申请大于可用空间的内存时将产生 OOM 的错误,错误的内存管理方式则会导致内存泄漏

v8 通过释放那些在 heap 不在被引用的对象的内存来达到内存释放的目的。
v8 的垃圾回收器用来回收并从用未被使用的内存

Minor GC (Scavenger)

将 new space 分为大小相等的两个部分。分别命名为 from-space、to-space。
每次都是从 from-space 中分配内存,如果发现 from-space 中的内存不够,就会进行一次 GC
GC 的过程可以概况如下

  • 遍历 from-space 中的所有对象并标记使用中的对象为 alive。
  • 将 alive 的对象复制到 to-space
  • 清空 from-space 中非 alive 对象
  • 交换 to-space 跟 from-space 的指针
  • 在 from-space 中分配新对象的内存

Major GC(Mark-Sweep-Compact)

标记-清扫-紧凑算法

It uses a tri-color(white-grey-black) marking system.
采用三色标记系统算法

  • 标记:这是两种算法共有的第一步,垃圾收集器在其中标识正在使用的对象和未使用的对象。从 GC 根目录(堆栈指针)递归使用或使用的对象被标记为活动对象。从技术上讲,这是对堆的深度优先搜索,可以视为有向图

  • 清除:垃圾收集器遍历堆并记下任何未标记为活动的对象的内存地址。现在,该空间在空闲列表中被标记为空闲,可用于存储其他对象

  • 压缩:清扫后,如果需要,所有使用中的对象将被移动到一起。这将减少碎片并提高为新对象分配内存的性能

这种类型的 GC 也被称之为(世界停止-时间暂停)GC,为了避免时间暂停,v8 引入了下面的技术

  • 增量 GC:GC 是在多个增量步骤中完成的,而不是一个。
  • 并发标记:标记是使用多个 helper thread 并发完成的,而不会影响 js main thread。写屏障用于在跟踪 js 创建对象之前的新引用的同时 helper thread 还能进行标记操作。
  • 并发清除/压缩:清除和压缩是在辅助线程中同时进行的,不影响 js main thread
  • lazy sweeping - 懒清除。
    延迟页面中垃圾的删除,直到需要内存为止。

其他文章

如何避免内存泄漏

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions