携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第N天,点击查看活动详情 >>
V8引擎在64位系统下最多只能使用约1.4GB的内存,在32位系统下最多只能使用约0.7GB的内存
为什么要有内存上限
内存大,垃圾多,回收勤, 现成阻塞 -> 页面卡住
Web Wroker 可以创建子线程,但是子线程完全受主线程控制,不能访问浏览器特定的API,例如操作DOM,因此这个新标准并没有改变JS单线程的本质。
栈区域垃圾回收
栈区存放的一般是执行上下文
- 从逻辑上的垃圾回收: 上下文弹出栈
- 从实际上的垃圾回收:指向栈顶的指针向下移动,之后入栈的执行上下文直接覆盖 已(逻辑)删除 的上下文
堆区域垃圾回收
常见的GC算法
-
引用计数:
核心思想:就是设置引用计数器,判断当前引用数是否为 0,通过一个引用计数器,当引用关系发生改变的时候,就会修改引用计数器的数字
缺点:无法回收循环引用的对象,比如 a.test =b , b.test =a
-
标记清除:
核心思想:图的可达性
缺点:空间碎片化
-
标记整理:标记清除的进阶版
-
分代回收
V8的垃圾回收策略
V8的垃圾回收策略主要是基于分代式垃圾回收机制,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法。
V8的内存结构
-
新生代(new_space) :大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。
-
老生代(old_space) :
- 新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。
- 老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。
-
大对象区(large_object_space) :存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。
-
代码区(code_space) :代码对象,会被分配在这里,唯一拥有执行权限的内存区域。
-
map区(map_space) :存放Cell和Map,每个区域都是存放相同大小的元素,结构简单(这里没有做具体深入的了解,有清楚的小伙伴儿还麻烦解释下)。
新生代
概念 新生代内存是由两个 semispace(半空间) 构成的,内存最大值在 64 位系统和 32 位系统上分别为 32MB 和 16MB,在新生代的垃圾回收过程中主要采用了Scavenge算法。
常规过程:
- 它将新生代内存 一分为二,每一个部分的空间称为 semispace
- 处于激活状态的区域我们称为 From 空间,未激活(inactive new space)的区域我们称为 To 空间。
- 这两个空间中,始终只有一个处于使用状态,另一个处于闲置状态。我们的程序中声明的对象首先会被分配到 From 空间,当进行垃圾回收时,
- 如果 From 空间中尚有存活对象,则会被复制到 To 空间进行保存,非存活的对象会被自动回收。当复制完成后,
From空间和To空间完成一次角色互换,To空间会变为新的From空间,原来的From空间则变为To空间。 - 其中对于From空间的整理是采用 标记整理
对象晋升:将新生代对象移动至老生代对象存储空间
- 一轮 GC 操作(Scavenge)后还存活的新生代对象
- 当 to 空间使用率超过 25%的时候,因为 To 之后会变成 From 空间给后续的对象进行分配,所以超过25%会认为不太好了进行分配
老生代
概念:采用新的算法Mark-Sweep(标记清除)和Mark-Compact(标记整理)
Mark-Sweep(标记清除) 分为 标记 和 清除 两个阶段,在标记阶段会遍历堆中的所有对象,然后标记活着的对象,在清除阶段中,会将死亡的对象进行清除。Mark-Sweep算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收,具体步骤如下:
- 垃圾回收器会在内部构建一个
根列表,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window全局对象可以看成一个根节点。 - 然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点(currentNode)不能到达的地方即为非活动的,将会被视为垃圾。
- 最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。
以下几种情况都可以作为根节点:
- 全局对象
- 本地函数的局部变量和参数
- 当前嵌套调用链上的其他函数的变量和参数
Mark-Component(标记整理) 回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存
前面有提到垃圾回收会阻塞JS线程 -> 增量标记 / 延迟清除 / 增量整理
Incremental Marking(增量标记) :即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。
延迟清理(lazy sweeping) 和 增量式整理(incremental compaction) ,让 清理 和 整理 的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入 并行标记 和 并行清理 ,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。
BTW:类似于 Fiber 的实现