垃圾回收

垃圾回收

为什么需要垃圾回收

V8引擎已经帮我们自动进行了内存的分配和管理,JS 不需要手动开辟和释放内存。

存在写代码的过程中不够严谨而容易引发内存泄漏的问题

V8引擎的内存限制

JS 是单线程,垃圾回收的过程阻碍了主线程逻辑的执行。会导致主线程的等待时间长,浏览器长时间得不到响应。

V8引擎直接粗暴的限制了堆内存的大小。在浏览器端一般也不会遇到需要操作几个G内存这样的场景。在node端我们可以手动调整。

经典垃圾回收策略

引用计数

引用计数会跟踪每个值被引用的次数,当引用数为0时变量,内存就会被释放。因为存在循环引用的问题,所以很少使用这种方法。

在代码运行时就会自动的增减,因此效率高。

标记清除

第一个阶段是标记,也就是找垃圾的过程,从根节点出发遍历对象,对所有访问过的对象打上标记,表示对象可达。第二阶段是清除,对那些没有标记的对象进行回收。以下几种情况都可以作为根节点:

  1. 全局对象
  2. 本地函数的局部变量和参数
  3. 当前嵌套调用链上的其他函数的变量和参数

因为需要暂停下来遍历对象,所以效率相对较低。

V8的垃圾回收策略

V8的内存结构

新生代(new_space)大多数的对象(如执行上下文)开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。

老生代(old_space)新生代中的对象在存活一段时间后就会被转移到老生代内存区(如全局变量、自定义类、函数等),相对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。

大对象区(large_object_space):存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。

代码区(code_space):代码对象,会被分配在这里,唯一拥有执行权限的内存区域。

map区(map_space):存放Cell和Map,每个区域都是存放相同大小的元素,结构简单(这里没有做具体深入的了解,有清楚的小伙伴儿还麻烦解释下)。

上图中的带斜纹的区域代表暂未使用的内存,新生代(new_space)被划分为了两个部分,其中一部分叫做inactive new space,表示暂未激活的内存区域,另一部分为激活状态。

新生代

新生代的垃圾回收过程中主要采用了**Scavenge**算法。

Scavenge算法是一种典型的牺牲空间换取时间的算法(不需要处理垃圾,只需要复制有用的对象),对于老生代内存来说,可能会存储大量对象,如果在老生代中使用这种算法,势必会造成内存资源的浪费,但是在新生代内存中,大部分对象的生命周期较短,在时间效率上表现可观,所以还是比较适合这种算法。

Scavenge算法的具体实现中,主要采用了Cheney算法,它将新生代内存一分为二,每一个部分的空间称为semispace,也就是我们在上图中看见的new_space中划分的两个区域,其中处于激活状态的区域我们称为From空间,未激活(inactive new space)的区域我们称为To空间。这两个空间中,始终只有一个处于使用状态,另一个处于闲置状态。我们的程序中声明的对象首先会被分配到From空间,当进行垃圾回收时,如果From空间中尚有存活对象,则会被复制到To空间进行保存,非存活的对象会被自动回收。当复制完成后,From空间和To空间完成一次角色互换,To空间会变为新的From空间,原来的From空间则变为To空间。

Scavenge算法的垃圾回收过程主要就是将存活对象在From空间和To空间之间进行复制,同时完成两个空间之间的角色互换,因此该算法的缺点也比较明显,浪费了一半的内存用于复制。

扫描过程

广度优先遍历 + 双指针

  • 假设全局变量下的变量A引用了变量B和变量C,变量B引用了变量D,变量E没被引用
1
2
3
4
5
6
7
8
9
ROOT

A E
↓→ → →
↓ ↓
B C

D

  • 那么在to区域,存在一个扫描指针和分配指针,起初都指向最开始
  • 然后A发现被全局引用,那么A拷贝到to区域,这时候扫描指针指向A,分配指针指向后面一位
1
2
3
A
↑ ↑
SM FP
  • 然后A又引用了B,这时候把B拷贝到to区域,放在A后面,扫描指针还是指向A,分配指针往后移动一位
1
2
3
A   B
↑ ↑
SM FP
  • 然后再看A又引用了C,这时候把C拷贝到to区域,放在B后面,扫描指针还是指向A,分配指针往后移动一位
1
2
3
A   B   C  
↑ ↑
SM FP
  • 然后发现A没有引用其他了,那将扫描指针往后移动一位指向B
1
2
3
A   B    C
↑ ↑
SM FP
  • 然后发现B没有引用其他了,那再将扫描指针往后移动一位指向C
1
2
3
A   B    C
↑ ↑
SM FP
  • 然后C也没有引用其他了,还剩下D,D不存在引用,所以不需要移动到to区域,这时候清空FROM区域,然后将FROM和To交换。
  • 这就完成了一次垃圾回收。

对象晋升

当一个对象在经过多次复制之后依旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时(在将对象从From空间复制到To空间之前),该对象会被直接转移到老生代中,这种对象从新生代转移到老生代的过程我们称之为晋升
对象晋升的条件主要有以下两个(满足任意一个则会晋升):

  • 对象是否经历过一次Scavenge算法
  • To空间的内存占比是否已经超过25%为了不影响后续FROM空间的分配

老生代

在老生代中,因为管理着大量的存活对象,如果依旧使用Scavenge算法的话,很明显会浪费一半的内存,因此已经不再使用Scavenge算法,而是采用新的算法**Mark-Sweep(标记清除)Mark-Compact(标记整理)**来进行管理。

标记清除的问题是在经历过一次标记清除后,内存空间可能会出现不连续的状态,因为我们所清理的对象的内存地址可能不是连续的,所以就会出现内存碎片的问题,导致后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,而这次垃圾回收其实是没必要的,因为我们确实有很多空闲内存,只不过是不连续的。

为了解决这种内存碎片的问题,Mark-Compact(标记整理)算法被提了出来,该算法主要就是用来解决内存的碎片化问题的,回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存

进一步优化

全停顿

增量标记

为了减少垃圾回收带来的停顿时间,V8引擎又引入了Incremental Marking(增量标记)的概念,即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。尽可能少地影响主线程的任务,避免应用卡顿,提升应用性能。

得益于增量标记的好处,V8引擎后续继续引入了延迟清理(lazy sweeping)增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。

快速总结

内存管理

新生代

老生代

大对象区

代码区

map区

新生代垃圾回收

Scavenge 算法,空间换时间。Scavange 的 实现采用了 Cheney 算法。将新生代分为 from 空间 和 to 空间,对象首先会被分到 from 空间,经过一次扫描(广度优先遍历+双指针),我们就知道了哪些对象仍然被引用,这些对象就会被复制到 to 区域,然后 from 区域清空,再将 from 和 to 交换 。

Scavenge 效率高在哪里?

不需要处理垃圾,只需要复制有用的变量即可。

相较于标记清除,需要给每个对象打标记。

老生代垃圾回收

标记清除 + 标记整理

标记清除的问题内存碎片(经历过一次标记清楚后,对象的内存地址不连续,剩余的零碎的空间无法放置连续的大对象,从而导致提前的垃圾回收。

标记整理解决了该问题:回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存。

进一步优化:

增量标记:将原本需要一次性遍历堆内存的操作改为增量标记的方式

对象晋升

新生代 —> 老生代

条件(满足任一):

1.对象是否经历过一次 Scavenge 算法

2.To 空间的内存占比是否超过 25%

如何避免内存泄漏

  1. 尽可能少地创建全局变量

会无形地挂载到window全局对象上,变成根节点,必须要将其设置为 null 才能回收

  1. 记得手动清除定时器

  2. 少使用闭包

  3. 使用弱引用

WeakMap, WeakSet

弱引用是指垃圾回收的过程中不会将键名对该对象的引用考虑进去,只要所引用的对象没有其他的引用了,垃圾回收机制就会释放该对象所占用的内存

参考

https://juejin.cn/post/6844904016325902344#heading-2

https://blog.csdn.net/qq_17175013/article/details/103759055

https://www.bilibili.com/video/BV1bP4y1u7GF?spm_id_from=333.337.search-card.all.click&vd_source=1616b746cefe2e7615c229563ba38eb4


垃圾回收
http://example.com/2022/06/18/垃圾回收/
Author
John Doe
Posted on
June 18, 2022
Licensed under