Js内存机制详解

概述

  • 和大部分脚本语言(as、lua、python…)一样,javascript的内存分配和回收也是自动完成的,满足一定条件,就会被垃圾回收器自动回收

    内存生命周期

  • 内存分配:申明函数、对象时,系统会自动为其分配内存
  • 内存使用:调用函数和对象时,读写内存
  • 内存回收:满足条件,内存释放

变量在内存中的存储

  • 堆内存:顺序随意,寄存速度比栈内存慢,由程序申请,操作简单,存储空间较大(取决于系统有效虚拟内存)
  • 栈内存:先进后出,寄存速度快,栈数据可共享,由系统自动分配,数据固定不够灵活,空间大小有限制,超出则栈溢出(window下为1(?2)M)
  • javascript中的基本数据类型(Undefined、Null、Boolean、Number 、String),分为变量标识和值,均保存在栈内存,变量标识指向其对应的值
  • 引用数据类型(非基本数据类型如:Array、Object…)相对较为复杂,分为值、值所在堆地址以及变量标识,其中值保存在堆内存,变量标识和值所在堆地址保存在栈内存,变量标识指向其对应的值所在堆地址
    1
    2
    3
    var a = "test";
    var b = new String('test');
    var c = [1,2,3];

其中a,b,c为变量标识,保存在栈内存;”test”为基本数据类型,也保存在栈内存,而new String('test')(需特别注意,new出来的对象均为Object)和[1,2,3]保存在堆内存,而标识他们的b,c之所以能找到它们,是因为生成这个对象的同时,栈内存中同时保存了值所在堆地址,b,c指向对应的值所在堆地址。

  • 理解上述知识后,就能很好的解决javascript中关于传值和传引用的相关问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var a = 'test';
    var b = {"key":"test"};
    var c = a;
    c = 'test1';
    var d = b;
    d['key'] = 'test1';
    console.log(a);//输出:test
    console.log(b);//输出:{"key":"test1"}
    console.log(c);//输出:test1
    console.log(d);//输出:{"key":"test1"}
    • 声明a并赋值"test",步骤:
      • 声明a,查找栈内存中是否存在a,不存在则创建,不管是已存在还是新创建的a,都指向undefined
      • a赋值,在栈内存中查找"test",无则栈内存中创建"test",然后让a指向"test"
    • 声明b并赋值{"key":"test"},步骤:
      • 声明b(同声明a
      • b赋值,堆内存中创建{"key":"test"},栈内存中创建其堆地址url_b,让b指向url_b
    • 声明c并赋值a,步骤:
      • 声明c(同声明a
      • c赋值,在栈内存中查找a,找不到则抛出错误,找到a则让c指向a所指向的test
    • c 赋值test1,步骤:
      • 在栈内存中查找c,未找到则声明一个全局对象c
      • c指向新的值"test1"
    • 声明d并赋值b,步骤:
      • 声明d(同声明a
      • d赋值,在栈内存中查找b,找不到则抛出错误,找到b则让d指向b所指向的url_b
    • d['key'] = 'test1',步骤:
      • 在栈内存中查找d,找不到则抛出错误,找到d则通过其所指向url_b找到堆内存中的{"key":"test"}
      • 修改堆内存中的{"key":"test"}{"key":"test1"}
    • 输出结果说明:
      • a指向test,输出test
      • b指向url_burl_b对应{"key":"test1"}在堆内存中的值,输出{"key":"test1"}
      • c指向test1,输出test1
      • d指向url_burl_b对应{"key":"test1"}在堆内存中的值,输出{"key":"test1"}

内存回收条件(和as3.0等类似)

  • 引用计数法:在内存中,没有其他人引用这个对象时,该对象被回收

    • 适用场景

      1
      2
      3
      4
      var a = 'test';//"test"引用次数:1
      var b = a;//"test"引用次数:2
      a = 1;//"test"引用次数:1
      b = 2;//"test"引用次数:0,满足被回收的条件
    • 局限:循环引用

      1
      2
      3
      4
      5
      6
      7
      function foo(){
      var c = {"key":"value"};
      var d = [1,2,3];
      c['key'] = d;
      d[0] = c;
      }
      foo();

      foo函数执行完毕,{"key":"value"}[1,2,3]被关在foo的作用域内,外面已经没有人可以使用它们,按理应该没有存在的价值,会被删除,然而因为它们之间循环引用,引用计数始终不为0,因而不满足引用计数法的内存回收条件,这种情况下,会使用另外一种算法

  • 标记清除法:在内存中,访问不到的对象可被回收
    • 这种算法要比引用计数法覆盖范围更大,命题引用计数为0则该对象无法访问成立,其逆命题不成立
    • 对象进入作用域,标记为1,出了其作用域,标记成0,javascript会隔一段时间进行扫描,发现有标记为0的对象,则回收。
    • 上述循环引用的例子中,执行完毕foo后堆内存中的{"key":"value"}[1,2,3],栈内存中的c,d,以及各自对应的堆地址,均被标记为0,可回收。