- 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 分配
内存的分配方式分为:
- 静态内存分配
- 动态内存分配
区别:
2.2 使用
读写基本变量或对象的属性、传参等操作,都涉及到了内存的使用。
2.3 释放
对于不再使用的内存,应当及时释放。
3 V8内存结构
V8的内存结构:
- 正在运行的程序由某些内存表示,这些内存称为常驻集(Resident Set)。
3.1 栈内存
栈用于静态内存分配(Static Memory Allocation),它具有以下特点:
-
操作数据快,因为是在栈顶操作
-
数据必须是静态的,数据大小在编译时是已知的
-
多线程应用程序中,每个线程可以有一个栈
-
堆的内存管理简单,且由操作系统完成
-
栈大小有限,可能发生栈溢出(Stack Overflow)
-
值大小有限制
3.2 堆内存
堆用于动态内存分配(Dynamic Memory Allocation),与栈不同,程序需要使用指针在堆中查找数据。它的特点是:
-
操作速度慢,但容量大
-
可以将动态大小的数据存储在此处
-
堆在应用程序的线程之间共享
-
因为堆的动态特性,堆管理起来比较困难
-
值大小没有限制
堆内存的进一步划分:
-
新生代(New space / Young generation):空间小,并且分为了两个半空间(Semi-space),由Minor GC(Scavenger)管理,其中的数据存活期短(short-lived)。
-
老生代(Old Space / Old generation):空间大,由Major GC(Mark-Sweep & Mark-Compact)管理。进一步分为:
-
2.1 旧指针空间(Old pointer space):包含的对象中还存在指针,这个指针指向其他对象
-
2.2 旧数据空间(Old data space):包含的对象中仅有数据
-
大对象空间(Large object space):这里对象的大小超过了其他空间大小限制。
-
代码空间(Code-space):即时(Just In Time,JIT)编译器在这里存储已编译的代码块。
-
元空间(Cell Space),属性元空间(Property Cell Apace),映射空间(Map Space):这些空间中的每个空间都包含相同大小的对象,并且对它们指向的对象有某种约束,从而简化了收集。
页(Page):页是从操作系统分配的连续内存块,以上的空间都由一组组的页构成的。
3.3 使用内存
一个可视化的案例,模拟了代码运行过程中Stack和Heap的分配情况:
4 回收栈内存
V8会通过移动记录当前执行状态的指针(ESP) 来销毁该函数保存在栈中的执行上下文。
5 回收堆内存
V8中的垃圾收集器(Garbage Collector),它的工作是:跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。并且,这个垃圾收集器是分代的,也就是说,堆中的对象按其年龄分组并在不同阶段清除。
回收堆内存有两种思路:
-
引用计数法(Reference-counting garbage collection)
-
标记清除法(Mark-and-sweep algorithm):从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法。并且,近年来JavaScript垃圾回收领域的改进均是基于这个算法进行的。
在V8中,使用两个阶段和三种算法来进行GC:
-
Minor GC:针对新生代,使用Scavenger和Cheney’s algorithm两种算法
-
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 标记清除法
标记清除法把“对象是否不再需要”简化定义为“对象是否可以获得”。
标记清除法的两个步骤:
-
标记:从根节点开始寻找可以到达的节点,并标记这些节点。
-
清除:垃圾回收器释放未标记的内存。
标记清除法解决了循环引用的问题。在之前的示例代码中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,它们将会被垃圾回收器回收。
5.2 V8 GC的两个步骤
5.2.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”的角色
-
不断重复上述过程
一个可视化的案例,模拟了Minor GC:
5.2.2 Major GC
Scavenger算法中需要涉及数据迁移,因此适用于小数据,但老生区的数据较大,因此不宜采用该种方法。
Major GC针对老生区进行垃圾回收,使用的是Mark-Sweep-Compact算法,思路为:
-
标记(Marking):对堆进行深度优先搜索(depth-first-search),标记可达对象
-
清除(Sweeping):垃圾收集器遍历堆并记下未标记为活动的对象的内存地址。现在,该空间在空闲列表中被标记为空闲,可用于存储其他对象。
-
压缩(Compacting):将所有存活的对象移到一起,以减少碎片化,并提高为新对象分配内存的性能
全停顿(stop-the-world GC):这种类型的GC方式也被称为全停顿,因为 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行,会造成性能的下降。
V8中采取的解决策略:
-
增量式GC(Incremental GC):GC是在多个增量步骤中完成的,而非一次性。
-
并发标记(Concurrent marking)和并发清理/压缩(Concurrent sweeping/compacting):使用多个帮助线程(helper threads)并发完成的,而不会影响主线程。
-
懒清理(Lazy sweeping):指的是延迟处理Page中的垃圾,直到需要内存才进行清理。
6 总结
-
JavaScript是自动GC,内存的生命周期分为“分配内存-使用内存-释放内存”三个阶段。
-
介绍了V8内存结构中的Heap及Stack,以及它们的特点和具体组成。
-
介绍了GC的两种思路:引用计数法和标记清除法,并且,目前的浏览器使用的是标记清除法。
-
重点介绍了V8中GC的两个阶段:Minor GC和Major GC。