JavaScript 内存机制
JavaScript的内存管理机制是:内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时“自动”释放。后者被称为垃圾回收
内存模型
JS内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)。 其中栈存放变量,堆存放复杂对象,池存放常量。
基础数据类型与栈内存
JS中的基础数据类型,这些值都有固定的大小,往往都保存在栈内存中(闭包除外),由系统自动分配存储空间。
我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问
数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。
基础数据类型: Number String Null Undefined Boolean
引用数据类型与堆内存
与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JS不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。
为了更好的搞懂栈内存与堆内存,我们可以结合以下例子与图解进行理解。
var a1 = 0; // 栈
var a2 = 'this is string'; // 栈 var a3 = null; // 栈
var b = { m: 20 }; // 变量b存在于栈中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3] 作为对象存在于堆内存中
|变量名|具体值|
|:--:|:--:|
|c|0x0012ff7d|
|b|0x0012ff7c|
|a3|null|
|a2|this is string|
|a1|0|
[栈内存空间] ------->
堆内存空间
[1,2,3] {m:20}
因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从栈中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。 理解了JS的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的一些特点了。 在前端面试中我们常常会遇到这样一个类似的题目
// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少? 20
// demo02.js
var m = { a: 10, b: 20 };
var n = m;
n.a = 15;
// 这时m.a的值是多少 15
内存的生命周期
JS环境中分配的内存一般有如下生命周期:
内存分配:当我们申明变量、函数、对象的时候,系统会自动为他 们分配内存
内存使用:即读写内存,也就是使用变量、函数等
内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
为了便于理解,我们使用一个简单的例子来解释这个周期。
var a = 20; // 在内存中给数值变量分配空间
alert(a + 100); // 使用内存
var a = null; // 使用完毕之后,释放内存空间
第一步和第二步我们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。
现在想想,从内存来看 null 和 undefined 本质的区别是什么?
为什么typeof(null) //object typeof(undefined) //undefined?
现在再想想,构造函数和立即执行函数的声明周期是什么?
对了,ES6语法中的 const 声明一个只读的常量。一旦声明,常量的值就不能改变。但是下面的代码可以改变 const 的值,这是为什么?
const foo = {};
foo.prop = 123;
foo.prop // 123
foo = {}; // TypeError: "foo" is read-only
内存回收
JavaScript有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?其实很简单,就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。
在JavaScript中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此
a = null 其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。
而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。
在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。
以Google的V8引擎为例,在V8引擎中所有的JAVASCRIPT对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。
另外,V8引擎对堆内存中的JAVASCRIPT对象进行分代管理。新生代:新生代即存活周期较短的JAVASCRIPT对象,如临时变量、字符串等; 老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。
function fun1() {
var obj = {name: 'csa', age: 24};
}
function fun2() {
var obj = {name: 'coder', age: 2}
return obj;
}
var f1 = fun1(); //下一次垃圾回收来临的时候,会释放
var f2 = fun2(); //下一次垃圾回收来临的时候,不会释放
在上述代码中,当执行var f1 = fun1();的时候,执行环境会创建一个{name:'csa', age:24}这个对象,当执行var f2 = fun2();的时候,执行环境会创建一个{name:'coder', age=2}这个对象,然后在下一次垃圾回收来临的时候,会释放{name:'csa', age:24}这个对象的内存,但并不会释放{name:'coder', age:2}这个对象的内存。
这就是因为在fun2()函数中将{name:'coder, age:2'}这个对象返回,并且将其引用赋值给了f2变量,又由于f2这个对象属于全局变量,所以在页面没有卸载的情况下,f2所指向的对象{name:'coder', age:2}是不会被回收的。
由于JavaScript语言的特殊性(闭包...),导致如何判断一个对象是否会被回收的问题上变的异常艰难
垃圾回收算法
核心思想就是如何判断内存已经不再使用了
标记清除算法
标记清除算法就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
内存泄露
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
WeakMap
有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就可以了。
ES6 考虑到了这一点,推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用。
下面以 WeakMap 为例,看看它是怎么解决内存泄漏的。
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。
也就是说,DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。