垃圾回收与内存泄漏
垃圾回收
内存生命周期
- 分配内存:当我们申请变量、函数、对象的时候,系统会自动为它们分配内存。
- 内存使用:即读写内存,也就是使用变量、函数等。
- 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存。
概念
GC是Garbage Collection,程序过程中会产生很多垃圾,这些垃圾是程序不再使用的内存或者一些不可达的对象,而GC就是负责回收垃圾的,找到内存中的垃圾、并释放和回收空间。
在浏览器的发展历史上,用到过两种主要的标记策略:标记清理
和 引用计数
。
垃圾回收的方式
引用计数
引用计数的核心思想是每个值都被记录它被引用的次数。创建一个对象并将其赋值给变量 a,此时,该对象的引用计数为 1。
如果该对象又被赋值给变量 b,那么引用计数加 1 变为 2。如果保存对该对象引用的变量被其他对象给覆盖了,那么该对象引用计数减一。当一个对象
的引用数为 0 时,就说明该对象可以被垃圾回收器安全地清除,以释放其占用的内存。
引用计数的优点:
- 可即刻回收垃圾, 当引用计数为 0 时,对象在变成垃圾的时候会立刻被回收。
- 因为是及时回收,不需要专门的垃圾回收程序,避免了长时间的垃圾回收暂停,从而提高了程序的运行效率。
引用计数的缺点:
- 时间开销大,因为引用计数算法需要维护引用数,一旦发现引用数发生改变需要立刻对引用数进行修改;
- 无法解决循环引用问题。循环引用就是比如函数中声明了对象 a 和对象 b,对象 a 有一个指针指向对象 b,而对象 b 也引用了对象 a,这样的情况下它们的引用数都是 2,并且永远不会变成 0,
如果函数被多次调用,则会导致大量内存永远不会被释放,就产生了内存泄漏问题。手动解决办法就是把变量设置为 null,切换变量与引用之间的联系,
当下次垃圾回收程序运行时,这些垃圾就会被回收。
标记清理
在代码执行阶段,为程序中所有变量添加上一个二进制字符,并初始值置为 0(默认全是垃圾),然后遍历所有对象,被使用的变量标记设置为 1,在
程序运行结束时回收掉所有标记为 0 的变量,回收结束后再将现存变量再设置为 0,等待下一轮回收开启。
标记清理的优点:
- 算法思路清晰,实现简单。
- 避免了循环引用的问题。
标记清理的缺点:
- 内存碎片化,造成空间浪费,并且新分配空间时导致分配时间过长。
- 不会立即回收资源。
减少垃圾回收的方法
代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化:在清空一个数组时,将数组的长度设置为 0,以此来达到清空数组的目的。
- 对
Object
进行优化:对象尽量复用,对于不再使用的对象就设置为 null,尽快被回收。 - 对函数进行优化:在循环中的函数表达式,如果可以复用就尽量放在循环外面。
内存泄漏
什么是内存泄漏
简单来说就是不再用到的对象内存没有及时被垃圾回收机制回收时,就叫做内存泄漏(Memory leak)
那些情况会导致内存泄漏
- 不正当的闭包,解决办法:在函数调用后,把外部的引用关系置空就行。
- 隐式全局变量:函数中没有声明而直接使用的变量就会造成隐式全局变量,这种变量在函数执行结束后不会被回收,就会造成内存泄漏。结局办法:尽量通过 let,const 定义局部变量。
- 定时器:
setInterval
没有结束前,回调函数里的变量以及回调函数本身都无法被回收。setTimeout
也存在同样的问题。解决办法:当不需要定时器时,调用clearInterval
和clearTimeout
来清除。 - 遗忘的事件监听器或监听者模式(eventBus):比较说 vue,在组件中挂载了事件处理函数,在组件销毁时不主动将其清除,其中引用的变量不会进行回收,可能引起内存占用过高,造成意外的内存泄漏。解决办法:在组件被销毁前的生命周期里清除即可,
removeEventListener
和eventBus.off
。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 缪克立的博客!