V8中的JavaScript的内存管理与垃圾回收

一.内存的生命周期

无论任何语言,内存生命周期大体一致,会分为:分配内存,使用内存,销毁内存三个阶段

upload successful

1.分配

1.1静态内存分配

1.2动态内存分配

区别:
静态内存分配:

  • 数据大小在编译的时候必须是已知的
  • 在编译时执行
  • 分配给栈
  • 先进后出

upload successful

2.使用

读写基本变量或对象的属性、传参等操作,都涉及到了内存的使用。

3.释放

对于不再使用的内存,应当及时释放。


二.V8的内存结构

正在运行的程序由某些内存表示,这些内存成为常驻集
upload successful

1.栈内存

栈用于静态内存分配(Static Memory Allocation),它具有以下特点:

  • 操作数据快,因为是在栈顶操作
  • 数据必须是静态的,数据大小在编译时是已知的
  • 多线程应用程序中,每个线程可以有一个栈
  • 堆的内存管理简单,且由操作系统完成
  • 栈大小有限,可能发生栈溢出(Stack Overflow)
  • 值大小有限制

2.堆内存

堆用于动态内存分配(Dynamic Memory Allocation),与栈不同,程序需要使用指针在堆中查找数据。它的特点是:

  • 操作速度慢,但容量大
  • 可以将动态大小的数据存储在此处
  • 堆在应用程序的线程之间共享
  • 因为堆的动态特性,堆管理起来比较困难
  • 值大小没有限制

堆的内存还可以进一步划分:
1.新生代:空间小,并且分为了两个半空间,由Minor GC管理,其中数据存活期短
2.老生代:空间大,由Major GC管理,老生代可以进一步划分为:

  • 旧指针空间:包含的对象中还存在指针,这个指针指向其他对象
  • 旧数据空间:包含的对象中仅有数据

3.大对象空间:这里对象的大小超过了其他空间大小限制
4.代码空间:即时编译器在这里存储已编译的代码块
5.元空间、属性元空间、映射空间:这些空间中的每个空间都包含相同大小的对象,并且对它们指向的对象有某种约束,从而简化了收集。

(页(Page):页是从操作系统分配的连续内存块,以上的空间都由一组组的页构成的。)


三.回收栈内存

V8会通过移动记录当前执行状态的指针来销毁该函数保存在栈中的执行上下文


四.回收堆内存

V8中的垃圾收集器(Garbage Collector),它的工作是:跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。并且,这个垃圾收集器是分代的,也就是说,堆中的对象按其年龄分组并在不同阶段清除。

回收堆内存有两种思路:
1.引用计数法
2.标记清除法

在V8中,使用两个阶段和三种算法来进行GC:
1.Minor GC:针对新生代,使用Scavenger和Cheney’s algorithm两种算法
2.Major GC:针对老生代,使用Mark-Sweep-Compact算法

两种思路:

1.引用计数法:

内存引用(Memory References)是引用计数法中的一个重要概念。在内存管理的上下文中,如果一个对象可以隐式或显式访问另一个对象,则称该对象引用另一个对象。 例如,JavaScript对象能够引用其原型(隐式引用)和其属性的值(显式引用)。

引用计数就是当某个对象的引用计数为0的时候,就把这个对象视为可回收垃圾

缺点:
如果出现循环引用的情况,那么就无法进行垃圾回收了。

1
2
3
4
5
6
7
8
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1 references o2
o2.p = o1; // o2 references o1. This creates a cycle.
}

f();

upload successful

2.标记清除法
标记清除法不再把“对象是否不再被需要”简化为对象是否被引用了,它是从根节点开始寻找节点,并标记可以到达的节点(标记),然后释放没有被标记的节点(清除)。

标记清除法解决了循环引用的问题。在之前的示例代码中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,它们将会被垃圾回收器回收。

const和let具有块级作用域,当块级作用域消失的时候就可以及时回收内存,提高程序运行效率

V8 GC的两个步骤

1.Minor GC

Minor GC是针对新生区的垃圾回收。
Minor GC的整体思路:(这个过程使用到了Scavenger和Cheney’s algorithm。)

  • 新生代分为两个半区,分别为“To-Space”和“from-Space”,我们先不断地在from-Space上分配内存
  • 如果from-Space满了,就触发GC
  • 找出from-Space上的活动对象,如果这个活动对象存活过两个minor GC周期,就把它移到老生代,否则把它们移到To-Space
  • 清空from-Space
  • 转换“To-Space”和“from-Space”的角色
  • 不断重复上述过程

2.Major GC

Scavenger算法中需要涉及数据迁移,因此适用于小数据,但老生区的数据较大,因此不宜采用该种方法。

Major GC针对老生区进行垃圾回收,使用的是Mark-Sweep-Compact算法,思路为:

  • 标记:对堆进行深度优先遍历,标记上所有可到达的对象
  • 清除:所有未被标记的对象的内存地址均为空闲的内存空间,可以用于存储其他对象
  • 压缩:将所有存活的对象移到一起,以减少碎片化,并提高为新对象分配内存的性能

全停顿(stop-the-world GC):这种类型的GC方式也被称为全停顿,因为 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行,会造成性能的下降。

V8中采取的解决策略:
1.增量式编程:GC是在多个增量步骤中完成的,而不是一次性
2.并发标记和并发清理/压缩:使用多个帮助线程并发完成的,而不会影响主线程
3.懒清理:指的是延迟处理Page中的垃圾,直到需要内存才进行清理。

upload successful


五.如何优化内存的使用

1.不要在循环之中定义函数
2.不要在循环中定义对象
3.清空数组

1
arr.length=0;

学习链接:
https://juejin.cn/post/6971245488058302477#heading-6
https://juejin.cn/post/6891614154134667272