不管什么程序语言,它的生命周期一般可以按顺序分为三个部分:
JavaScript 是在创建变量时自动进行了分配内存,并且在不使用它们时“自动”释放,释放的过程称为垃圾回收。
JavaScript 是一种弱类型的、动态的语言。
- 弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JS引擎在运行代码的时候自己会计算出来。
- 动态,可以使用同一个变量保存不同类型的数据。码的时候自己会计算出来。动态,意味着你可以使用同一个变量保存不同类型的数据。123456var barbar = 12 bar = “ 极客时间 “bar = truebar = nullbar = {name:” 极客时间 “}复制代码

JS的内存空间 -> 调用栈的中数据回收 -> JS引擎通过向下移动ESP(记录当前执行状态的指针)来销毁该函 数保存在栈中的执行上下文
-> 堆中的数据回收 -> 垃圾回收器 -> 标记清除算法 -> WeakMap、WeakSet
-> 分代收集 、增量收集
-> 常见的内存泄漏问题
JS的内存空间
在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间**。
- 原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的。堆中的数据是通过引用和变量关联起来的
- 原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。
标记清除算法
JavaScript 中主要的内存管理概念是 可达性。
如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。
垃圾回收器在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。
根:
- 当前函数的局部变量和参数。
- 嵌套调用时,当前调用链上所有函数的变量与参数。
- 全局变量。
垃圾回收的基本算法被称为 “mark-and-sweep”。
定期执行以下“垃圾回收”步骤:
- 垃圾收集器找到所有的根,并“标记”(记住)它们。
- 然后它遍历并“标记”来自它们的所有引用。
- 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
- ……如此操作,直到所有可达的(从根部)引用都被访问到。
- 没有被标记的对象都会被删除。
垃圾回收器将定期从全局对象 window 开始,找所有从 window 开始引用的对象,然后找这些对象引用的对象……递归完成后,垃圾回收器将找到所有“可获得”的对象和收集所有“不能获得”的对象,最后将不可获得的对象进行回收。
问题:
通常,当对象、数组这类数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都是可以访问的。
例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。
let john = { name: "John" };let array = [ john ];john = null; // 覆盖引用// 前面由 john 所引用的那个对象被存储在了 array 中// 所以它不会被垃圾回收机制回收
类似的,如果使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也将存在。它会占用内存,并且应该不会被(垃圾回收机制)回收。
let john = { name: "John" };let map = new Map();map.set(john, "...");john = null; // 覆盖引用// john 被存储在了 map 中,// 我们可以使用 map.keys() 来获取它
WeakMap 在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object)的回收。
分代收集
代际假说:
- 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
- 第二个是不死的对象,会活得更久
不管什么类型的垃圾回收器的共同执行过程**
- 第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
- 第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
- 第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。
这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如副垃圾回收器。
V8的堆分为新生代 -> 生存时间短的对象 , 1~8M的容量 -> 副垃圾回收器
老生代 -> 生存时间长的对象 , 容量大 -> 主垃圾**回收器
副垃圾回收器:
小的对象非配到新生区, 频繁;
Scavenge算法:
把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。**
在垃圾回收过程中,
- 首先要对对象区域中的垃圾做标记;
- 标记完成之后,进入垃圾清理阶段,副垃圾回收器会把这些存活的对象**复制**到空闲区域中,同时它还会把这些对象有序地排列起来,这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。
- 完成复制后,对象区域与空闲区域进行角色翻转,原来的对象区域 <——> 空闲区域,这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
复制操作需要时间成本, 为了执行效率,一般新生区的空间会被设置得比较小。
空间不大,所以很容易被存活的对象装满整个区域, 采用了对象晋升策略,经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
主垃圾回收器
负责老生代: 包括: 新生代中晋升的对象 和 一些大的对象
特点: 对象占用空间大
存活时间长
标记清除算法:**
- 标记过程阶段。
标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
- 垃圾的清除过程
- 对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。
- 另外一种算法——标记 - 整理(Mark-Compact)

全停顿
一旦执行垃圾回收算法,都需要将正在执行的JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。如果时间太长,会造成页面的卡顿.
增量标记算法
降低老生代的垃圾回收而造成的卡顿, 将标记过程分为一个个的子标记过程
且让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。
