开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情
javascript中的垃圾收集
- javascript使用的自动内存管理,被成为“垃圾回收机制”(GC)
- 优点是可以简化开发,节省代码
- 缺点是无法完整的掌握内存的分配与回收的具体过程
v8内存管理
v8内存限制
- 在64位操作系统约为1.4G内存
- 在32位操作系统约为0.7G内存
内存管理
- js对象都是通过v8进行分配管理内存的
- process.memoryUsage 返回一个对象,包含了Node进程的内存占用信息
- res:所有内存的占用,包括指令区和堆栈。
- heapTotal:“堆”占用的内存,包括用到的和没用到的
- heapUsed:用到的堆的部分,判断内存泄漏,以heapused字段为准
为什么要限制内存大小呢
- 因为v8垃圾收集工作原理导致的,1.4G内存完全一次垃圾收集需要1s以上
- 回收的时候会让浏览器一直处于等待的状态。严重影响应用程序的性能
v8垃圾回收机制
- 基于分代的垃圾回收
- 不同代的垃圾回收机制不一样
- 按存活的时间分为新生代和老生代
分代
年龄小的是新生代,由from区域和to区域组成
- 64位:占32M,from区域各占16M
- 32位:占16M,from区域各占8M
年龄大的是老生代,
- 64位:占1400M
- 32位:占700M
新生代垃圾回收
- 新生代区域一分为二,每个16M,一个使用,一个空闲
- 开始垃圾回收时候,会检查from区域中的存活对象,如果活着,拷贝到TO空间,完成后释放空间
- 完成后FROM和TO互换
- 新生代扫描的时候是一种广度优先的扫描策略
- 新生代空间小,存活对象少
scavenge算法
新生代采用的是
scavenge算法
Scavenge算法是一种典型的牺牲空间换取时间的算法,对于老生代内存来说,可能会存储大量对象,如果在老生代中使用这种算法,势必会造成内存资源的浪费,但是在新生代内存中,大部分对象的生命周期较短,在时间效率上表现可观,所以还是比较适合这种算法。
在Scavenge算法的具体实现中,主要采用了Cheney算法,它将新生代内存一分为二,每一个部分的空间称为semispace,也就是我们在上图中看见的new_space中划分的两个区域,其中处于激活状态的区域我们称为From空间,未激活(inactive new space)的区域我们称为To空间。这两个空间中,始终只有一个处于使用状态,另一个处于闲置状态。我们的程序中声明的对象首先会被分配到From空间,当进行垃圾回收时,如果From空间中尚有存活对象,则会被复制到To空间进行保存,非存活的对象会被自动回收。当复制完成后,From空间和To空间完成一次角色互换,To空间会变为新的From空间,原来的From空间则变为To空间。
晋升
一个对象经历过多次的垃圾回收依然存活的时候,生存周期比较长的对象会被移动到老生代,这个移动过程被称为
晋升
- 经过5次以上的回收还存在
- TO的空间使用占比超过25% 或超大对象
老生代
在老生代中,因为管理着大量的存活对象,如果依旧使用Scavenge算法的话,很明显会浪费一半的内存,因此已经不再使用Scavenge算法,而是采用新的算法Mark-Sweep(标记清除)和Mark-Compact(标记整理)来进行管理。
引用计数
就是看对象是否还有其他引用指向它,如果没有指向该对象的引用,则该对象会被视为垃圾并被垃圾回收器回收。存在弊端,所以这种方法就被放弃了
function foo() {
let a = {};
let b = {};
a.a1 = b;
b.b1 = a;
}
foo();
这个例子中我们将对象a的a1属性指向对象b,将对象b的b1属性指向对象a,形成两个对象相互引用,在foo函数执行完毕后,函数的作用域已经被销毁,作用域中包含的变量a和b本应该可以被回收,但是因为采用了引用计数的算法,两个变量均存在指向自身的引用,因此依旧无法被回收,导致内存泄漏。
Mark-Sweep(标记清除)
Mark-Sweep(标记清除)分为标记和清除两个阶段,在标记阶段会遍历堆中的所有对象,然后标记活着的对象,在清除阶段中,会将死亡的对象进行清除。Mark-Sweep算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收
但是Mark-Sweep算法存在一个问题,就是在经历过一次标记清除后,内存空间可能会出现不连续的状态,因为我们所清理的对象的内存地址可能不是连续的,所以就会出现内存碎片的问题,导致后面如果需要分配一个大对象而空闲内存不足以分配.
Mark-Compact(标记整理)
该算法主要就是用来解决内存的碎片化问题的,回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存,
全停顿(stop-the-world)
垃圾回收的过程会阻碍主线程同步任务的执行,待执行完垃圾回收后才会再次恢复执行主任务的逻辑,这种行为被称为全停顿(stop-the-world)
增量标记(Incremental Marking)
将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。否则延迟执行,尽可能少地影响主线程的任务,避免应用卡顿,提升应用性能。