Blog

内存管理与垃圾回收

time: 2019.01.22

在此之前,我也是认为 javascript 宿主环境会自己实现垃圾回收,开发人员不用去管理内存的释放

目录

1 内存分配

为什么需要了解浏览器内存分配呢?做性能优化必备

性能优化:

  1. 页面的性能随着时间的延长越来越差,内存泄漏
  2. 页面的性能一直差:浏览器内存一直占很大内存,也叫内存膨胀
  3. 页面出现延迟或者经常暂停,垃圾回收

内存特指渲染器进程,因为我们的dom解析、js执行都是在渲染进程中执行。那么内存分为:

  1. vm 堆内存:特指 js 内存,存储 js 对象数据,比如 object, array 等;也包含堆数字、堆字符串
  2. 其他渲染器内存:存储浏览器原生对象、dom tree、调用栈、闭包对象、EventListener等,一些可以通过内存快照查看

代码空间:存储代码语句
栈空间:存储调用栈,也就是存储的执行上下文对象,保存了变量数据。其中引用类型在这里保存的是一个引用地址值
堆空间:vm js 对象内存,保存引用类型数据的真实值

2 垃圾回收

2.1 gc 根

gc 根:垃圾回收的起始节点,内部由 js 对象实例句柄组成。
从应用角度触发,分为如下2种:

  1. window Global 对象
  2. dom 树 另外,开发者也可以主动控制不释放(console.log, debugger)
    所有 vm 垃圾回收都是从 gc 根出发

2.2 垃圾回收算法

高级语言的解释器(宿主环境中的)嵌入了垃圾回收器,但是没有办法明确知道什么时候不再用它了,只能做一个近似的判断,所以在大型项目中,如果不主动回收内存,会存在很多内存被浪费了。

在 js 中,内存回收多是指的 vm 内存回收,也就是从 gc 根不能到达的对象需要回收。
那么如果是被普通变量引用,也可以说是被调用栈引用着的,也不会被回收,那么该怎么理解呢?
答:调用栈中的执行上下文对象内存,在当前代码执行完毕之后,内存自然也就被释放了;如果存在闭包对象,那么会保留当前执行上下文,待引用闭包对象的变量释放之后,则释放闭包内存

垃圾回收算法目前有2种:

  1. 老生区 - 1.4g, 标记清除
  2. 新生区 - 32m, scanverge

老生区:用于存放活跃较久的堆数据(在新生区经历2次回收还存活的)、体积大的数据,使用标记清除算法回收垃圾,主要是慢速回收垃圾
新生区:存放体积小的数据,使用 scanverge 回收垃圾,主要是快速回收垃圾

scanverge:新生区由活跃区和空闲区构成,活跃区快满时执行垃圾回收;结束回收之后,则将活跃区数据复制到空闲区,让分散数据连续起来,优化内存分配;活跃区和空闲区对换,循环 标记清除:从 gc 根出发,定时扫描内存中的对象,凡是能够从根部到达的对象,则保留,否则无法触及到的对象则标记为 无法到达的对象 ,稍后清除。通常是分阶段逐步回收,因为数据量大会消耗挺长时间来回收垃圾

内存释放:hello = null

特殊:es6 提供了 WeakSet 和 WeakMap,表示对象的弱引用,是不计算入垃圾回收机制的。

2.3 gc 任务执行时机

  1. 内存到达临界点:新生区 from 块内存到达临界点,则执行垃圾回收,然后将活跃对象复制到 to 块,并交换 from 和 to 概念;老生区即将满时也会触发 gc 任务
  2. 申请较大内存时,老生区可能会执行一次 gc。往往老生区的 gc 耗时更长,因为其体积较大

开发者使用 memory takeSnapshot 也会触发 gc

2.4 gc 优化

gc 优化的本质是减少 gc 任务的执行

  1. 减少系统内存的使用:比如页面组件过多
  2. 减少对象频繁生成:申请新内存,往往伴随 gc 任务,可以利用对象池技术,缓存对象来实现 gc 优化

3 内存泄漏

可以查看 系统性能衡量及优化-内存泄漏,来判断是否存在内存泄漏

对于持续运行的服务进程,必须及时释放不再用的内存,否则内存占用越来越高,轻则影响系统性能,重则进程崩溃。

内存泄漏:对于不再使用到的内存,没有及时释放。

要知道内存泄漏的原理,就需要熟悉 标记清除 这个垃圾回收算法,总结一下它的特点

  1. 从全局对象出发,垃圾回收器创建了一个 root 列表
  2. 递归遍历检查,能达到的标记为激活,不能达到的标记为失效,稍后回收

6种常见的js内存泄漏

3.1 意外的全局变量

创建了额外的全局变量,没有使用了,垃圾回收器不会主动回收。

通常未定义的变量也叫做全局变量:

function hello() {
    world = 'hello world';
}

解决方案:

  1. 使用严格模式 "use strict"
  2. 使用完毕将全局变量赋值为 null

问题:垃圾回收器既然从全局对象出发,这种定义的全局变量也是挂载在全局对象上的,为什么不主动回收呢?
猜想:垃圾回收器从全局对象出发,必须要是显示声明 window.hello 才行

3.2 定时器

如果使用 setInterval、setTimeout 定时任务,但是最后又不需要了,却没有清除它,则会造成内存泄漏

3.3 脱离 dom 的引用

let hello = document.getElementById('button');
document.body.removeChild(document.getElementById('button'));

此刻 hello 还是保存了这个按钮对象,虽然按钮对象已经不再 dom 树中了。可以通过 memory 快照中 Detached HTMLBUTTONELEMENT 来查看。

3.4 闭包

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};

setInterval(replaceThing, 1000);

闭包环境没有被释放:不是所有定义了没有用的变量都会被垃圾回收器回收,如果该变量值是一个函数对象,函数内部引用了其他变量,构成闭包,那么即使该变量没有地方用到,也不会被垃圾回收器回收,会因为引用了其他变量的原因,持久保存在内存中,需要开发者主动释放。

3.5 EventListener

绑定到 document 等全局对象的事件,如果事件具柄内使用了一些引用对象,则可能会存在内存泄漏,因为这些事件如果不清除,则相关的引用对象也不会被释放。

3.6 开发者控制台

在控制台输出对象、断点调试程序时,相应的内存也不会被释放

参考文章

mdn 内存管理
github 木易杨 内存机制
github 木易杨 内存泄漏及如何避免
chrome-devtools/memory
深度理解浏览器中的垃圾回收算法