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

时间:2021-1-8 作者:admin

  • 1 前言
  • 2 内存生命周期
    • 2.1 分配
    • 2.2 使用
    • 2.3 释放
  • 3 V8内存结构
    • 3.1 栈内存
    • 3.2 堆内存
    • 3.3 使用内存
  • 4 回收栈内存
  • 5 回收堆内存
    • 5.1 两种思路
    • 5.2 V8 GC的两个步骤
  • 6 总结
  • 资料

1 前言

在一些低级语言中,比如:C语言,我们可以使用malloc()free()手动控制内存的分配与释放。但是,在JavaScript里,垃圾回收(Garbage Collection)会自动地帮我们完成内存的分配与释放。尽管如此,我们仍然有必要了解JavaScript中的内存管理。

2 内存生命周期

无论使用何种语言,内存的生命周期(Memory Life Cycle)大体一致,可以分为以下三个阶段:

2.1 分配

内存的分配方式分为:

  1. 静态内存分配
  2. 动态内存分配

区别:

2.2 使用

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

2.3 释放

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

3 V8内存结构

V8的内存结构:

  • 正在运行的程序由某些内存表示,这些内存称为常驻集(Resident Set)。

3.1 栈内存

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

  1. 操作数据快,因为是在栈顶操作

  2. 数据必须是静态的,数据大小在编译时是已知的

  3. 多线程应用程序中,每个线程可以有一个栈

  4. 堆的内存管理简单,且由操作系统完成

  5. 栈大小有限,可能发生栈溢出(Stack Overflow)

  6. 值大小有限制

3.2 堆内存

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

  1. 操作速度慢,但容量大

  2. 可以将动态大小的数据存储在此处

  3. 堆在应用程序的线程之间共享

  4. 因为堆的动态特性,堆管理起来比较困难

  5. 值大小没有限制

堆内存的进一步划分:

  1. 新生代(New space / Young generation):空间小,并且分为了两个半空间(Semi-space),由Minor GC(Scavenger)管理,其中的数据存活期短(short-lived)。

  2. 老生代(Old Space / Old generation):空间大,由Major GC(Mark-Sweep & Mark-Compact)管理。进一步分为:

  • 2.1 旧指针空间(Old pointer space):包含的对象中还存在指针,这个指针指向其他对象

  • 2.2 旧数据空间(Old data space):包含的对象中仅有数据

  1. 大对象空间(Large object space):这里对象的大小超过了其他空间大小限制。

  2. 代码空间(Code-space):即时(Just In Time,JIT)编译器在这里存储已编译的代码块。

  3. 元空间(Cell Space),属性元空间(Property Cell Apace),映射空间(Map Space):这些空间中的每个空间都包含相同大小的对象,并且对它们指向的对象有某种约束,从而简化了收集。

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

3.3 使用内存

一个可视化的案例,模拟了代码运行过程中Stack和Heap的分配情况:

4 回收栈内存

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

5 回收堆内存

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

回收堆内存有两种思路

  1. 引用计数法(Reference-counting garbage collection)

  2. 标记清除法(Mark-and-sweep algorithm):从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法。并且,近年来JavaScript垃圾回收领域的改进均是基于这个算法进行的。

在V8中,使用两个阶段三种算法来进行GC:

  1. Minor GC:针对新生代,使用Scavenger和Cheney’s algorithm两种算法

  2. Major GC:针对老生代,使用Mark-Sweep-Compact算法

5.1 两种思路

5.1.1 引用计数法

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

引用计数法的思想是:一旦对某个对象的引用数为0,则把这个对象视为可收集垃圾(Garbage Collectible)。

可以通过以下示例代码,进一步了解引用计数法的原理。

var o1 = {
  o2: {
    x: 1
  }
};
// 2 objects are created. 
// 'o2' is referenced by 'o1' object as one of its properties.
// None can be garbage-collected

var o3 = o1; // the 'o3' variable is the second thing that 
            // has a reference to the object pointed by 'o1'. 

o1 = 1;      // now, the object that was originally in 'o1' has a         
            // single reference, embodied by the 'o3' variable

var o4 = o3.o2; // reference to 'o2' property of the object.
                // This object has now 2 references: one as
                // a property. 
                // The other as the 'o4' variable

o3 = '374'; // The object that was originally in 'o1' has now zero
            // references to it. 
            // It can be garbage-collected.
            // However, what was its 'o2' property is still
            // referenced by the 'o4' variable, so it cannot be
            // freed.

o4 = null; // what was the 'o2' property of the object originally in
           // 'o1' has zero references to it. 
           // It can be garbage collected.

引用计数法存在着一个缺点——循环引用。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。然而,由于它们互相都有至少一次引用,所以它们不会被回收。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1. This creates a cycle.
}

f();

5.1.2 标记清除法

标记清除法把“对象是否不再需要”简化定义为“对象是否可以获得”。

标记清除法的两个步骤:

  1. 标记:从根节点开始寻找可以到达的节点,并标记这些节点。

  2. 清除:垃圾回收器释放未标记的内存。

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

5.2 V8 GC的两个步骤

5.2.1 Minor GC

Minor GC是针对新生区进行垃圾回收。

Minor GC的总体思路:(这个过程使用到了Scavenger和Cheney’s algorithm。)

  1. 新生代分为两个半区,分别为“To-Space”和“from-Space”,我们先不断地在from-Space上分配内存

  2. 如果from-Space满了,就触发GC

  3. 找出from-Space上的活动对象,如果这个活动对象存活过两个minor GC周期,就把它移到老生代,否则并把它们移到To-Space

  4. 清空from-Space

  5. 转换“To-Space”和“from-Space”的角色

  6. 不断重复上述过程

一个可视化的案例,模拟了Minor GC:

5.2.2 Major GC

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

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

  1. 标记(Marking):对堆进行深度优先搜索(depth-first-search),标记可达对象

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

  3. 压缩(Compacting):将所有存活的对象移到一起,以减少碎片化,并提高为新对象分配内存的性能

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

V8中采取的解决策略:

  1. 增量式GC(Incremental GC):GC是在多个增量步骤中完成的,而非一次性。

  2. 并发标记(Concurrent marking)和并发清理/压缩(Concurrent sweeping/compacting):使用多个帮助线程(helper threads)并发完成的,而不会影响主线程。

  3. 懒清理(Lazy sweeping):指的是延迟处理Page中的垃圾,直到需要内存才进行清理。

6 总结

  1. JavaScript是自动GC,内存的生命周期分为“分配内存-使用内存-释放内存”三个阶段。

  2. 介绍了V8内存结构中的Heap及Stack,以及它们的特点和具体组成。

  3. 介绍了GC的两种思路:引用计数法和标记清除法,并且,目前的浏览器使用的是标记清除法。

  4. 重点介绍了V8中GC的两个阶段:Minor GC和Major GC。

资料

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。