总结Ⅹ-JS垃圾回收&内存泄露

650 阅读4分钟

垃圾回收

1.什么是垃圾

JS会在创建变量时自动分配内存,在不使用的时候会自动周期性的释放内存,释放的过程就叫 "垃圾回收"。这个机制有好的一面,当然也也有不好的一面。一方面自动分配内存减轻了开发者的负担,开发者不用过多的去关注内存使用,但是另一方面,正是因为因为是自动回收,所以如果不清楚回收的机制,会很容易造成混乱,而混乱就很容易造成"内存泄漏"。

  1. 所有全局变量都不是垃圾
  2. 所有window对象都不是垃圾,如Array、Object、Promise
  3. 局部变量在退出函数时就会变成垃圾
  4. 如果没有人引用某一部分变量,那么这部分就会变成垃圾
    • 例1:a指向某一个对象,某一时刻a被置为null,那么这个对象就会被删除,因为没有别人引用这个对象
    • 例2:如果三个对象互相引用(环引),但是global中没有人引用他们,可以把这三个对象看成一个整体。即就算他们相互引用,也会被视作垃圾。

2.如何捡垃圾(遍历和计数,只是不同的算法而已)

一、标记-清除算法 Mark-Sweep GC

如字面意思 mark-sweep 分为两个阶段:

  1. 标记阶段:从根集合出发,将所有活动对象及其子对象打上标记

  2. 清除阶段:遍历堆,将非活动对象(未打上标记)的连接到空闲链表上

image.png

  • 优点

    实现简单, 容易和其他算法组合

  • 缺点

    • 碎片化, 会导致无数小分块散落在堆的各处
    • 分配速度不理想,每次分配都需要遍历空闲列表找到足够大的分块
    • 与写时复制技术不兼容,因为每次都会在活动对象上打上标记

二、引用计数 Reference Counting

引用计数,就是记录每个对象被引用的次数,每次新建对象、赋值引用和删除引用的同时更新计数器,如果计数器值为0则直接回收内存。 很明显,引用计数最大的优势是暂停时间短

  • 优点

    • 可即刻回收垃圾
    • 最大暂停时间短
    • 没有必要沿指针查找, 不要和标记-清除算法一样沿着根集合开始查找
  • 缺点

    • 计数器的增减处理繁重
    • 计数器需要占用很多位
    • 实现繁琐复杂,每个赋值操作都得替换成引用更新操作
    • 循环引用无法回收

内存泄露

1.什么是内存泄漏?

内存泄漏Memory Leak是指程序中已动态分配的堆内存由于疏忽或错误等原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

2.常见内存泄漏场景

  1. 意外的全局变量

    在JavaScript中并未严格定义对未声明变量的处理方式,即使在局部函数作用域中依旧能够定义全局变量,这种意外的全局变量可能会存储大量数据,且由于其是能够通过全局对象例如window能够访问到的,所以进行内存回收时不认为其是需要回收的内存而一直存在,只有在窗口关闭或者刷新页面时才能够被释放,造成意外的内存泄漏

  2. 引用计数算法

    引用计数垃圾回收算法有个限制,当对象循环引用时,就会造成内存泄漏,也就是引用计数垃圾回收算法无法处理循环引用的对象。

  3. 闭包

    闭包是JavaScript开发的一个关键方面,闭包可以让你从内部函数访问外部函数作用域,简单来说可以认为是可以从一个函数作用域访问另一个函数作用域而非必要在函数作用域中实现作用域链结构。由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多,在不再需要的闭包使用结束后需要手动将其清除。

  4. 被遗忘的事件监听器

    当事件监听器在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这样就造成意外的内存泄漏。