垃圾回收

内存生命周期

  1. 分配内存:当我们申请变量、函数、对象的时候,系统会自动为它们分配内存。
  2. 内存使用:即读写内存,也就是使用变量、函数等。
  3. 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存。

概念

GCGarbage Collection,程序过程中会产生很多垃圾,这些垃圾是程序不再使用的内存或者一些不可达的对象,而GC就是负责回收垃圾的,找到内存中的垃圾、并释放和回收空间。

在浏览器的发展历史上,用到过两种主要的标记策略:标记清理引用计数

垃圾回收的方式

引用计数

  • 引用计数的核心思想是每个值都被记录它被引用的次数。创建一个对象并将其赋值给变量 a,此时,该对象的引用计数为 1。

  • 如果该对象又被赋值给变量 b,那么引用计数加 1 变为 2。如果保存对该对象引用的变量被其他对象给覆盖了,那么该对象引用计数减一。当一个对象
    的引用数为 0 时,就说明该对象可以被垃圾回收器安全地清除,以释放其占用的内存。

引用计数的优点:

  1. 可即刻回收垃圾, 当引用计数为 0 时,对象在变成垃圾的时候会立刻被回收。
  2. 因为是及时回收,不需要专门的垃圾回收程序,避免了长时间的垃圾回收暂停,从而提高了程序的运行效率。

引用计数的缺点

  1. 时间开销大,因为引用计数算法需要维护引用数,一旦发现引用数发生改变需要立刻对引用数进行修改;
  2. 无法解决循环引用问题。循环引用就是比如函数中声明了对象 a 和对象 b,对象 a 有一个指针指向对象 b,而对象 b 也引用了对象 a,这样的情况下它们的引用数都是 2,并且永远不会变成 0,
    如果函数被多次调用,则会导致大量内存永远不会被释放,就产生了内存泄漏问题。手动解决办法就是把变量设置为 null,切换变量与引用之间的联系,
    当下次垃圾回收程序运行时,这些垃圾就会被回收。

标记清理

在代码执行阶段,为程序中所有变量添加上一个二进制字符,并初始值置为 0(默认全是垃圾),然后遍历所有对象,被使用的变量标记设置为 1,在
程序运行结束时回收掉所有标记为 0 的变量,回收结束后再将现存变量再设置为 0,等待下一轮回收开启。

标记清理的优点

  1. 算法思路清晰,实现简单。
  2. 避免了循环引用的问题。

标记清理的缺点

  1. 内存碎片化,造成空间浪费,并且新分配空间时导致分配时间过长。
  2. 不会立即回收资源。

减少垃圾回收的方法

代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。

  • 对数组进行优化:在清空一个数组时,将数组的长度设置为 0,以此来达到清空数组的目的。
  • Object进行优化:对象尽量复用,对于不再使用的对象就设置为 null,尽快被回收。
  • 对函数进行优化:在循环中的函数表达式,如果可以复用就尽量放在循环外面。

内存泄漏

什么是内存泄漏

简单来说就是不再用到的对象内存没有及时被垃圾回收机制回收时,就叫做内存泄漏(Memory leak)

那些情况会导致内存泄漏

  1. 不正当的闭包,解决办法:在函数调用后,把外部的引用关系置空就行。
  2. 隐式全局变量:函数中没有声明而直接使用的变量就会造成隐式全局变量,这种变量在函数执行结束后不会被回收,就会造成内存泄漏。结局办法:尽量通过 let,const 定义局部变量。
  3. 定时器:setInterval没有结束前,回调函数里的变量以及回调函数本身都无法被回收。setTimeout也存在同样的问题。解决办法:当不需要定时器时,调用clearIntervalclearTimeout来清除。
  4. 遗忘的事件监听器或监听者模式(eventBus):比较说 vue,在组件中挂载了事件处理函数,在组件销毁时不主动将其清除,其中引用的变量不会进行回收,可能引起内存占用过高,造成意外的内存泄漏。解决办法:在组件被销毁前的生命周期里清除即可,removeEventListenereventBus.off