JavaScript进阶之内存管理

172 阅读7分钟

数据类型

说到js中的的内存管理,那就不得不提到js的数据类型,js中的数据类型分为基础数据类型和引用类型。

数据类型有:

  • 基本数据类型: 字符串(String) 数字(Number) 布尔型(Boolean) 空对象(Null) 未定义(Undefined) Symbol(ES6新增)。

  • 引用类型: Object

内存空间:栈内存(stack)和堆内存(heap)

基础数据类型与栈内存:

基本数据类型的值是存储在内存空间的栈内存(stack)中的,这些基础类型的值的大小都是固定的,由系统自动分配内存空间,我们对其的操作都是直接对值的操作。 我们的栈结构就和我们的水桶是一样的,进水和倒水都只能从上面的口子完成,栈的进栈和入栈都是从顶部完成,

例如: 我定义了3个变量,这段代码js首先会对它进行解析编译得到3个变量的定义,然后在执行时对这3个变量进行赋值,根据赋值类型来决定在内存中的,由于都是基础类型,所以值是存储在栈内存中,根据赋值的顺序得到它们在栈内存的图如下

从一个栈删除元素叫做出栈或者退栈,它是把栈顶的元素删除,使其相邻的元素成为新的栈顶元素,遵循后入先出,先入后出的规则。

引用类型与堆内存:

我们知道基础数据类型是由系统自动分配内存空间,js中的引用数据类型如数组(Array)和对象(Object)它们的大小是不固定的,相对于基础数据类型在栈内存中存储,引用类型是存在于堆内存中,由于js是无法直接操作堆内存的,我们在操作对象时是无法操作堆内存的,所以我们存储在栈内存中的变量赋值的其实是对象在堆内存中存储的一个引用地址。

因此,js中操作对象实际上是操作对象在堆内存中的地址的引用,而不是实际的对象,所以,当我们工作中遇到不同的变量引用同一个地址时,会出现修改对象a后,对象b也跟着变了,这是因为2个对象都是引用的同一个地址。还有函数也是保存的引用地址,函数在堆内存中会以字符串的形式保存,只有当正真的执行函数调用时才会把字符串转换成可执行的js代码。

var a = { name: '小明'};
var b = a;
var c = a;
b.name = '小红';
console.log(b.name);         //输出  小红
console.log(c.name);         //输出  小红 ,因为b和c都引用了同一个地址,所以当改变a时b也会跟着改变

function fn(){
  console.log('hello js');
}
console.log(fn)    
//输出如下图

垃圾回收机制

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存一
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放\归还

所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在js中,大部分都是隐含的。

垃圾回收机制到底是什么呢?

我们要了解垃圾回收机制是什么就不得不了解它产生的背景,对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。在C语言中,是要开发者去手动的释放内存,而在js解释器中嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它,这就叫垃圾回收机制。

引用计数垃圾收集

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

示例:

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有

var oa = o2.a; // 引用“这个对象”的a属性
               // 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
           // 但是它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
           // 它可以被垃圾回收了

该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

IE 8以下 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:

var div;
window.onload = function(){
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("*");
};

在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的 DOM 元素,即使其从DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),而这个数据占用的内存将永远不会被释放。

标记-清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

循环引用不再是问题了 在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。 限制: 那些无法从根对象查询到的对象都将被清除 尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。