JavaScript 垃圾回收 和 EventLoop

210 阅读5分钟

JavaScript 垃圾回收机制

js 的内存管理

js 的数据类型分为基本数据类型和引用数据类型。

基本数据类型

一般保存在栈内存中,在内存中占据固定的内存空间,直接可以通过值来访问

引用数据类型

由于引用数据类型,大小不固定,栈内存存放的内存地址指向堆内存中的对象,是通过引用来访问。

总结:栈内存中的基本数据类型,可以通过操作系统直接处理,而堆内存中的引用数据类型,正是由于可以经常变化,大小不固定,因此需用通过 JS 引擎的垃圾回收机制来处理

内存回收

浏览器的垃圾回收只针对堆内存中的引用类型进行垃圾回收

新生代内存回收

新生代中内存变量存活时间比较短,因此不需要分配较大的内存空间去存春变量。

新生代的垃圾回收,会将内存分为两部分。浏览器考试进行垃圾回收时,浏览器会将左边内存使用的变量,复制到右边空间去,如果没有使用过的对象,直接进行垃圾回收。对象没有了之后,左右两部分内存进行对调,再进行上面的步骤。 其中产生的内存碎片,会使用 scavenge 算法进行整理。

老生代内存回收

新生代内存经过回收之后依然一直存在,就会被放到老生代内存中进行回收。就是经历过一次 scavenge 算法回收,就可以晋升为老生代内存的对象

由于 scavenge 的算法是有适用场景的,对于内存空间较大的,就不适用于 scavenge 算法了

采用的是标记清除和标记整理的策略进行垃圾回收

标记清除

分为标记阶段和清除阶段

标记阶段:首先会遍历堆上的所有对象,分别对他们打上标记,代码执行结束之后,对使用过的变量取消标记

清除阶段:把所有标记的变量进行整体清除,从而释放内存空间

标记清除完成之后,还是会产生内存碎片,会对之后的存储造成影响

标记整理

对标记清除完成后,产生的内存碎片,进行整理,往一段靠拢。

内存泄漏与优化

内存泄漏是指已经分配堆内存地址的对象由于长时间未释放或者无法释放,造成长期占用内存,造成内存浪费,最终导致运行的应用响应速度变慢以及最终崩溃的情况。

内存泄漏的原因

  1. 过多的缓存未释放
  2. 闭包太多未释放
  3. 定时器或者回调太多未释放
  4. 太多无效的dom未释放
  5. 全局变量太多未被发现

浏览器的事件循环 (EventLoop)

  1. JavaScript 引擎首先从宏任务队列中取出第一个任务
  2. 执行完毕之后,再将微任务中所有的任务取出,按照顺序分别全部执行(如果这一过程中产生了新的微任务,也需要执行)
  3. 然后再从宏任务中取出下一个,执行完毕后,再将微任务队列中的任务全部取出,循环往复,直到两个任务队列都执行完

总结:一次 EventLoop 循环会处理一个宏任务和所有这次循环中产生的微任务。

宏任务 & 微任务

宏任务

方法:

  1. 渲染事件(解析DOM,计算布局,绘制)
  2. 用户交互事件(鼠标点击,滚动页面,放大缩小等)
  3. setTimeout、setInterval等
  4. 网络请求完成 & 文件读写完成事件

执行时间不准确,有时间颗粒度。

微任务

微任务就是一个异步执行的函数,执行时机是主函数执行完成之后,当前宏任务结束之前

方法:

  1. MutationObserver 监控某个DOM节点,或者为这个节点添加、删除部分子节点,当 DOM 发生变化时,就会产生DOM变化记录的微任务。
  2. 使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

总结:

  1. 微任务和宏任务是绑定的,每个宏任务执行时,会创建自己的微任务队列
  2. 微任务的执行时长会影响当前宏任务的执行时长(比如在执行一次宏任务中,里面有十个微任务,每次微任务的执行时长是10ms,十个就延长了100ms)
  3. 在一个宏任务中,分别创建了一个用于回调的宏任务和微任务的,无论什么情况下,微任务都早于宏任务

MutationObserver (微任务)

出现原因:

出现之前,都是使用 setTimeout 和 setInterval 来轮询检测dom变化

  • 如果时间设置过长,DOM变化响应不够及时
  • 如果时间间隔设置过短,又会浪费很多无用的工作量去检测DOM

MutationObserver 可以用来监听 DOM 的变化,包括属性的变更、节点的增加、内容的改变等。

MutationObserver 采用了 ‘微任务+异步’ 的策略。

  • 通过异步操作解决了同步操作的性能问题
  • 通过微任务解决了实时性的问题

Process.nextTick (微任务 node.js)

process.nextTick(callback[, ...args])

  1. process.nextTick 会将 callback 添加到 ‘next tick queue’
  2. 'next tick queue' 会在当前 JavaScript stack 执行完成后,下一次EventLoop 开始执行前按照FIFO出队
  3. 如果递归调用 Process.nextTick 可能会导致一个无限循环,需要去适时终止递归