JavaScript执行机制之垃圾回收


title: js垃圾回收

前言

我们已经了解变量的赋值,以及引用类型和堆内存之间的关系。

前面的章节也多次提到垃圾回收,这里终于可以展开来讲。

内存回收

JS中有一个自动垃圾收集机制的, 垃圾收集器会每隔一段时间就执行一次释放操作, 去清理掉那些不再使用的值, 来释放它们占用的内存.

销毁局部变量和全局变量

1. 局部变量的销毁

对于一般的局部变量, 即便它们是存在于函数中, 当这个函数执行完了之后, 它里面的变量还是会被GC

唯一的特例是闭包。

闭包中的变量并不会随着函数的执行完毕而被清除掉,反而会一直保留着,除非这个闭包被清除(确保闭包中涉及的变量再也没有被别的函数引用到).

这也是对于一些第三方插件,在软件的一些生命周期中,需要手动设置null销毁的原因。

插件的编写由于开放封闭原则,会大量使用闭包。

2. 全局变量的销毁

一般不用自己销毁,垃圾回收会自动处理。

除非引用了闭包。


V8引擎的内存限制

JavaScript 引擎, 在使用的时候对系统内存的占用有大小限制.

对于我们熟悉的V8引擎来说(谷歌浏览器内核), 它只能使用系统的一部分内存.

  • 64位系统下能使用约1.4GB;
  • 32位系统下能使用约0.7GB.

这在浏览器端大部分情况下都够用, 但在特定场景,以及到了 node 就成了性能瓶颈。

例如想要一个 2G 的文件, 那么它就无法将其全部读入内存且进行其他的操作.

JS中的存储, 分为栈存储和堆存储.

  1. 对于栈内存, 当ESP指针(栈指针)下移,也就是上下文切换之后,栈顶的空间会自动被回收.
  2. 而对象的存储是通过堆来进行分配的, 当在构建一个对象且进行赋值操作的时候, JS会将相应的内存分配到堆上. 所以每创建一个对象之后, 堆就会大一点.

前面说了, V8引擎只能使用系统的一部分内存, 你的堆却可能会不停的增大, 直到大小达到了V8引擎的内存上限为止.

这就导致我们对于 V8引擎 的使用,存在内存限制。

V8 引擎为什么会设置一个内存的上限?

  • JS的单线程执行机制
  • JS垃圾回收机制的限制

JS中, 由于它是单线程运行的, 一次只能做一件事,。那就意味着一旦进入了垃圾回收阶段, 其它的运行逻辑都得暂停了, 得等它过了这个阶段才继续执行.

但垃圾回收是一件非常耗时的事情, 以 1.5GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要50ms 以上,做一次非增量式的垃圾回收甚至要 1s 以上.

所以若是垃圾回收时常过久的话, JS代码会一直没有响应, 造成应用卡顿.

于是V8干脆给它限制了堆内存大小, 这样就算你到顶了也不会说太卡, 而且其实大部分情况也不会说有操作几个G的情况, 因此这也是V8的一种权衡.

V8的内存限制是不可修改的吗🤔️?

并不是的, 你可以通过执行以下命令来修改它:

// 这是调整老生代这部分的内存,单位是MB。后面会详细介绍新生代和老生代内存
node --max-old-space-size=2048 xxx.js 

// 这是调整新生代这部分的内存,单位是 KB。
node --max-new-space-size=2048 xxx.js

《nodejs 前端项目编译时内存溢出问题的原因及解决方案》


堆内存的分代管理

V8引擎对堆内存中的JS对象进行了分代管理, 也就是分为 新生代老生代.

首先让我们来了解以下几个知识点:

  • 新生代 就是临时分配的内存,存活时间短, 如临时变量、字符串等;
  • 老生代 是常驻内存,存活的时间长, 如主控制器、服务器对象等;
  • V8的堆内存, 就是两个内存之和.

就像下面的这张图一样:

新生代内存的回收

其实也像图里画的一样, 新生代的默认内存限制很小:

  • 64位系统下为32MB;
  • 32位系统下为16MB.

确实是够小的啦, 主要原因是新生代中的变量存活时间短,来了马上就走,不容易产生太大的内存负担,因此可以将它设的足够小.

新生代内存结构

新生代内存会被分为两个部分:

memory2.png

一块叫做From, 另一块叫做To. (别的教材中是这么命名的, 后来我去找寻原因, 发现大概是因为在V8的源码-内存管理中有from_space_to_space_这两个东西吧)

  • From表示正在使用的内存;
  • To表示目前闲置的内存.
Scavenge算法

上面已经介绍了新生代内存的结构, 下面来说说它具体是如何进行垃圾回收的.

当进行垃圾回收的时候, 会经过以下几个步骤:

  1. V8From部分的对象全部检查一遍;
  2. 检查出若是 存活对象 则复制到To内存中, 若不是则直接回收;
  3. 复制到To内存中是按照顺序从头放置的;
  4. From中所有的存活对象全部复制完毕之后, FromTo就会 对调 , 也就是From被闲置, To在使用;
  5. 如此循环.

一张图方便你理解🤔:

memory3.png

不就是个清理垃圾的动作吗? 为什么V8要整的这么复杂啊, 又是遍历又是复制的.

而且为什么还要在To内存中按照顺序从头放置呢🤔️?

其实, 它这样做是有一定好处的, 首先让我们来看看下面这张图:

memory4.png

在上图中, 黄色的部分是待分配的内存, 而蓝色的小方块就是存活对象.

看起来存活对象非常的散乱, 使得空间变得零零散散, 并且堆内存又是连续分配的, 若是碰到稍微大点的对象的话都没有办法进行空间分配了.

堆包含一个链表来维护已用和空闲的内存块。在堆上新分配(用 new 或者 malloc)内存是从空闲的内存块中找到一些满足要求的合适块。所以可能让人觉得只要有很多不连续的零散的小区域,只要总数达到申请的内存块,就可以分配。

但事实上是不行的,这又让人觉得是不是零散的内存块不能连接成一个大的空间,而必须要一整块连续的内存空间才能申请成功.

(原文链接:https://blog.csdn.net/jin13277480598/article/details/54409543)

而这种零散的空间也有一个名字, 叫做 内存碎片.

因此将其按照顺序从头放置也是为了解决 内存碎片 的问题, 在一顿复制之后, To内存会被排列的整整齐齐的:

memory5.png

整顿之后就大大方便了后续连续空间的分配.

上面👆说的这种新生代垃圾回收算法也被叫做 Scavenge算法 (scavenge的本意就是回收).

所以这个Scavenge算法不仅仅是将非存活对象给回收了, 还需要对内存空间做整顿.

就像是我们平常打扫房间, 不仅仅是将不要的垃圾清理掉, 还顺便把房间内的东西给放整齐了😊.

老生代内存的回收

如果新生代中的变量经过多次回收之后依然存在的话, 它就会发生“晋升”, 被放入老生代内存中.

产生晋升的情况:

  • 已经经历过一次Scavenge回收;
  • To(闲置内存)空间的内存不足75%.

通过上面👆的介绍我们已经知道, 老生代内存的空间会比新生代的大了很多, 而且老生代累计的变量空间一般都是很大的.

因此老生代的垃圾回收就不能用Scavenge算法了, 一是会浪费一半的空间, 二对庞大的内存空间进行复制本身就是个“很重的体力活”.

标记清除

所以对于老生代的垃圾回收干脆粗暴点吧, 采用标记清除的方式进行回收.

标记清除主要是经过以下几个过程:

  1. 遍历堆中的所有对象, 给它们做上标记;
  2. 之后对于代码环境中使用的变量被强引用的变量取消标记(被标记的都是垃圾);
  3. 依然被标记的变量当成垃圾给清除掉, 进行空间的回收;

当然, 和新生代一样, 在清理了之后, 还要整理内存碎片, 当然它的整理办法就是在清理阶段结束后把存活对象全部往一端靠拢.

memory6.png

所以总的来说, 对于老生代内存的回收主要就是经过:

  • 标记清除阶段, 留下存活对象;
  • 整理阶段, 把存活对象往一边靠拢.

因此, 对于现在的主流浏览器来说, 只要切断对象与根部的关系, 就可以将对象进行回收.

并发标记

在上面我们已经介绍过了V8在进行垃圾回收的时候, 不可避免地会阻塞业务逻辑的执行, 特别如果是老生代垃圾回收的任务比较繁重的时候, 会很耗时严重影响应用的性能.

为优化解决此问题, V8官方在2018年推出了名为增量标记的技术.

总的来说该技术的作用就是将原本一口气完成的标记任务分为了很多小的部分去完成, 每完成一个小任务就停一会, 让js逻辑执行一会, 然后再继续执行下面的部分.

在 GC 扫描和标记活动对象时,它允许 JavaScript 应用程序继续运行

memory7.png

其实它内部并没有上面👆说的这么简单, 还是有很多实现机制的, 具体的可以看这里:

《引擎V8推出“并发标记”,可节省60%-70%的GC时间》

在通过增量标记后, 垃圾回收过程对JS应用的阻塞时间减少到原来了1 / 6, 可以说这优化相当大了啊.

总结

V8 内存限制:为了垃圾回收不影响性能,V8 引擎存在内存限制,在 64 位系统下为 32MB; 32 位系统下为 16MB

V8 分代管理:V8 的内存空间,存在新生代与老生代。新生代时临时内存,空间小。老生代是常驻内存,空间大。新生代内存经过多次内存分配回收,若仍旧存在,会晋升到老生代。

V8 垃圾回收:

①标记清除:给所有变量/对象打上标记,有引用就先清除,有标记就是垃圾。

②并发清除:将标记任务分成多段小任务, 每完成一个小任务就停一会, 让js逻辑执行一会, 然后再继续执行下面的部分。

参考文章:

扩展阅读

本文严格来看仍旧不够严谨,部分地方含糊不清,也比较片面(只强化了“栈堆与闭包”这个相关知识点的理解)。

在文章更新之前,对于垃圾回收若有更深的理解,仍旧需要参考其他文章。

推荐以下链接

理解v8的垃圾清理_weixin_43378716的博客-CSDN博客

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值