返回

揭秘JavaScript:你无法忽视的垃圾回收和内存泄露

前端

浏览器在处理内存回收时有各种算法,浏览器从诞生至今使用过很多不同的垃圾回收算法。不论我们采用何种回收方式,都会出现一个问题:什么情况下JS对象不会被垃圾回收器回收?

事实上,解决这一问题也是各个浏览器的重要课题,毕竟,JS对象是浏览器在运行过程中消耗内存的最大户,若是JS对象不能被回收,那么内存很快就会被JS对象占据完。JS对象何时会被回收,决定因素只有一个:该JS对象是否处于根对象的可达范围内。

在JS引擎中,会有一个全局的、默认的根对象,这个根对象不会被垃圾回收回收,因此,被根对象引用的JS对象永远不会被回收。而除了根对象外,哪些对象可以成为根对象呢?这一类对象成为可达根对象,而它们的共同点是,该对象是V8引擎可以直接访问到的对象,这一类对象包括:

  • 全局对象:即window对象,包括在window对象上直接声明的变量、对象和函数,这些变量、对象和函数在页面销毁前都不会被垃圾回收器回收,window本身不会被GC。
  • 函数的参数:函数执行完毕,函数的参数引用将自动消失,而被引用对象是否被回收,还要看被引用对象是否处于可达范围之内。
  • 在JavaScript代码里,函数内部的变量,当执行完毕后,就会销毁,而某些没有被声明的变量,其实被添加到 window 对象上。比如 var variable=1,会自动添加到 window.variable=1。
  • 未关闭的定时器:很多应用为实现某些功能,会采用setTimeout或setInterval开启计时器。但如果忘记关闭这些计时器,计时器对应的函数还会被保存在内存中,导致对应变量不能被释放。
  • 从JavaScript向浏览器注册事件的回调函数(addEventListener 注册事件)

当然,除了这些常见的JS对象外,还有一些不太常见的JS对象也有可能被视为可达根对象,如

  • 页面当前正在执行代码中创建的所有变量和对象。
  • 绑定事件的事件处理函数。
  • 所有已经被setTimeout和setInterval调度的函数。
  • 在页面的整个生命周期内都存在的对象。
  • 通过DOM属性建立的JS对象,这些JS对象不会被自动销毁,需要手动释放。
  • 被DOM节点引用的JS对象,由于DOM节点在整个页面中都是可访问的,所以只要DOM节点还在,被DOM节点引用的JS对象也不会被回收。

为了避免上述问题,在开发中我们需要对可能产生内存泄露的代码位置和方式进行检测,一般而言,以下位置和方式容易产生内存泄露:

  • 全局变量:全局变量的生命周期从声明开始到JS引擎销毁结束,在全局作用域下定义的变量均属于全局变量。由于全局变量一旦声明就不释放,所以容易导致内存泄露。
  • 闭包:闭包是指函数访问外部作用域中的变量或对象,从而导致外部作用域中的变量或对象在函数执行完毕后仍然存在于内存中。
  • 定时器:定时器是用来执行一次或多次任务的函数,定时器在执行完毕后,会被添加到执行队列中,等待下次执行。如果定时器没有被取消,那么定时器对应的函数就会一直存在于内存中。
  • DOM元素:DOM元素是网页的组成部分,DOM元素一旦被创建,就会一直存在于内存中。如果DOM元素没有被删除,那么DOM元素对应的JS对象也会一直存在于内存中。

内存泄漏的危害非常大,可能会导致浏览器崩溃、网页卡顿,严重的还会导致系统崩溃。为了避免内存泄露,在开发中需要对可能产生内存泄露的代码位置和方式进行检测,一般而言,以下位置和方式容易产生内存泄露:

  • 全局变量:全局变量的生命周期从声明开始到JS引擎销毁结束,在全局作用域下定义的变量均属于全局变量。由于全局变量一旦声明就不释放,所以容易导致内存泄露。
  • 闭包:闭包是指函数访问外部作用域中的变量或对象,从而导致外部作用域中的变量或对象在函数执行完毕后仍然存在于内存中。
  • 定时器:定时器是用来执行一次或多次任务的函数,定时器在执行完毕后,会被添加到执行队列中,等待下次执行。如果定时器没有被取消,那么定时器对应的函数就会一直存在于内存中。
  • DOM元素:DOM元素是网页的组成部分,DOM元素一旦被创建,就会一直存在于内存中。如果DOM元素没有被删除,那么DOM元素对应的JS对象也会一直存在于内存中。