调用堆栈(3)--内存机制(回收,泄漏)

内存回收

js有自动垃圾回收机制,垃圾收集器会每隔一段时间执行一次释放操作,找出那些不再使用的值,将其释放

  • 全局变量回收: 很难自动判断哪些需要回收,开发中应尽量避免使用全局变量
  • 局部变量回收: 局部作用域中,当函数执行完毕,其中变量不需要了,就会被回收
    V8引擎对堆内存中js对象进行分代管理
  • 新生代: 存活周期较短的对象,如临时的变量,字符串等
  • 老生代: 多次回收后依然存在的,周期较长,如主控制器,服务器对象等

垃圾回收算法

垃圾回收算法的核心思想是找出不在使用的内存,将其释放,常见有两种方法;

  • 引用计数(现代浏览器不用)
  • 标记清除(常用)
  • 分代回收

引用计数

看一个对象有没有其他引用,没有就说明没用,就被释放

1
2
3
4
5
6
7
8
9
10
11
12
// 创建一个对象person,他有两个指向属性age和name的引用
var person = {
age: 12,
name: 'aaaa'
};

person.name = null; // 虽然name设置为null,但因为person对象还有指向name的引用,因此name不会回收

var p = person;
person = 1; //原来的person对象被赋值为1,但因为有新引用p指向原person对象,因此它不会被回收

p = null; //原person对象已经没有引用,很快会被回收

引用计数的致命问题是存在循环引用时,将不会被释放
常见的dom操作也是循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn () {
let obj = {}
let obj2 = {}
obj.a = obj2
obj2.a = obj
return ''
}
fn()
// -------------------
var div = document.createElement("div");
div.onclick = function(el) {
console.log(el);
};

函数执行完毕,obj之间的引用依然存在,因此不会被回收
dom操作时,div引用事件函数,事件函数也引用div,因为函数内部能访问div
因此现在浏览器不再使用,但ie还在用

标记清除

标记清除算法中将’不再使用的对象’定义为’无法触达的对象’,即从根部(全局对象)出发,无法触达的对象会被标记,稍后释放.无法触达的对象包括没有引用的对象,但有引用无法触达依旧会被清理(如上述循环引用).
标记-清除算法包含三个步骤:

  • 根:一般来说,根是代码中引用的全局变量。就拿 JavaScript 来说,window 对象即是可看作根的全局变量。Node.js 中相对应的变量为 “global”。垃圾回收器会构建出一份所有根变量的完整列表。
  • 随后,算法会检测所有的根变量及他们的后代变量并标记它们为激活状态(表示它们不可回收)。任何根变量所到达不了的变量(或者对象等等)都会被标记为内存垃圾。
  • 通常标记行为发生在变量进入执行上下文(标记激活)和离开上下文(标记回收)
  • 最后,垃圾回收器会释放所有非激活状态的内存片段然后返还给操作系统。

标记清除算法的缺点地址不连续,空间碎片化;在V8引擎采用标记清除法与分代回收法,分代回收解决了这个问题。

假如B和A为被清理空间,B是两个域大小,A是一个域大小,此时新申请1.5个域大小,则B会空闲0.5,A不够用。

分代回收

  • 新生代

    新生代垃圾回收采用Scavenge 算法;分配给常用内存和新分配的小量内存

    • 内存大小
      • 32位系统16M内存
      • 64位系统32M内存
    • 分区:新生代内存分为以下两区,内存各占一半
      • From space 实际运行的分区
      • To space 空闲的分区
    • Scavenge算法
      • 当From space内存使用将要达到上限时开始垃圾回收,将From space中的不可达对象都打上标记
      • 将From space的未标记对象复制到To space。
        • 复制时将会排序,避免不连续情况
      • 然后清空From space、将其闲置,也就是转变为To space,俗称反转。
      • 缺点:只能使用一半的内存,空间换时间

新生代何时变为老生代:内存大小达到From space的25%;经历了From space <-> To space的一个轮回

  • 老生代

    老生代采用mark-sweep标记清除和mark-compact标记整理;通常存放较大的内存块和从新生代分配过来的内存块

    • 内存大小

      • 32位系统700M左右
      • 64位系统1.4G左右
    • 分区

      • Old Object Space 存放的是新生代分配过来的内存。
      • Large Object Space 存放其他区域放不下的较大的内存,基本都超过1M
      • Map Space 存放存储对象的映射关系
      • Code Space 存储编译后的代码
    • 回收流程

      • 遍历
        • 采用深度优先遍历,遍历每个对象。
        • 首先将非根部对象全部标记为1类,然后进行深度优先遍历。
        • 遍历过程中将对象压入栈,这个过程中对象被标记为2类。
        • 遍历完成对象出栈,这个对象被标记为3类。
        • 整个过程直至栈空
      • Mark-sweep
        • 标记完成之后,将标记为1类的对象进行内存释放

    • Mark-compact 标记整理

      垃圾回收完成之后,内存空间是不连续的。
      这样容易造成无法分配较大的内存空间的问题,从而触发垃圾回收。
      所以,会有Mark-compact步骤将未被回收的内存块整理为连续的内存空间。
      频繁触发垃圾回收会影响引擎的性能,内存空间不足时也会优先触发Mark-compact
      内存不足时也会优先Mark-compact

Scavenge中只复制活着的对象,而Mark-Sweep只清理死亡对象。 活对象在新生代中只占较小部分,死对象在老生代中只占较小部分,这是两种回收方式能高效处理的原因。


内存泄漏

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

  • 最常见的内存泄漏多与dom操作有关

    1
    2
    3
    4
    let box = {}
    box.msg = document.createElement('div')
    body.appendChild(box.msg)
    body.removeAllChild()

    虽然dom元素在body中清除,但其引用还在box中,如果box存在,则该dom对象将不会被清除

  • 意外的全局变量

    1
    2
    3
    4
    function fn () {
    bar = 1
    this.msg = 'aa'
    }

    未用var或let等声明的变量或函数内this指向全局时,定义的变量都会挂在全局,全局变量很难自动回收

  • 被遗忘的计时器或回调函数

    1
    2
    3
    setInterval(function () {}, 30)
    var el = document.getElementById('app')
    el.addEventListener('click', cb)

    计时器不终止会一直存在;
    回调在ie中属于循环引用,不会被处理,需remove监听;在现代浏览器中没事

  • 闭包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function fn () {
    let num = 1
    return function () {
    num += 1
    if (num===4) { // 特定条件释放闭包
    num = null
    return false
    }
    return num
    }
    }
    let f = fn()
    f() // 2
    f() // 3
    f = null // 释放

    形成闭包,需添加条件释放,否则将不会被回收


内存溢出

  • 没有足够的内存供申请者使用
  • 内存泄漏的堆积最终会导致内存溢出

内存管理

使用对象池

对象池(英语:object pool pattern)是一种设计模式。一个对象池包含一组已经初始化过且可以使用的对象,而可以在有需求时创建和销毁对象。池的用户可以从池子中取得对象,对其进行操作处理,并在不需要时归还给池子而非直接销毁它。这是一种特殊的工厂对象。

优化tips

1、尽可能避免创建对象,非必要情况下避免调用会创建对象的方法,如 Array.slice、Array.map、Array.filter、字符串相加、$(‘div’)、ArrayBuffer.slice 等。
2、不再使用的对象,手动赋为 null,可避免循环引用等问题。
3、使用 Weakmap;对象为键,值可任意类型;作为对象的键属于弱引用(),随时能被回收;参考

强引用
我们常见的普通对象的引用 例如Object object = new Object();
特点:只要强引用指向一个对象,就表明这个对象是”活的”
弱引用
弱引用一旦被垃圾回收器检测到,就会被回收。

4、生产环境勿用 console.log 打印对象,包括 DOM、数组、ImageData、ArrayBuffer 等。因为 console.log 的对象不会被垃圾回收。
5、合理设计页面,按需创建对象/渲染页面/加载图片等,避免一次性请求全部数据,避免重复dom操作,避免一次性加载渲染大量全部图片。
6、ImageData 对象是 JS 内存杀手,避免重复创建 ImageData 对象。