简介
每种编程语言都有它的内存管理机制,比如简单的C有低级的内存管理基元,像malloc()
,free()
。同样我们在学习JavaScript的时候,很有必要了解JavaScript的内存管理机制。 JavaScript的内存管理机制是:内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时“自动”释放。后者被称为垃圾回收。这个“自动”是混淆并给JavaScript(和其他高级语言)开发者一个错觉:他们可以不用考虑内存管理。 对于前端开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。当然也包括我自己。在很长一段时间里认为内存空间的概念在JS的学习中并不是那么重要。可是后我当我回过头来重新整理JS基础时,发现由于对它们的模糊认知,导致了很多东西我都理解得并不明白。比如最基本的引用数据类型和引用传递到底是怎么回事儿?比如浅复制与深复制有什么不同?还有闭包,原型等等。 但其实在使用JavaScript进行开发的过程中,了解JavaScript内存机制有助于开发人员能够清晰的认识到自己写的代码在执行的过程中发生过什么,也能够提高项目的代码质量。
内存模型
JS内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)。 其中栈存放变量,堆存放复杂对象,池存放常量。
基础数据类型与栈内存
JS中的基础数据类型,这些值都有固定的大小,往往都保存在栈内存中(闭包除外),由系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问 数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。 基础数据类型: Number
String
Null
Undefined
Boolean
复习一下,此问题常常在面试中问到,然而答不出来的人大有人在 ~ ~ 要简单理解栈内存空间的存储方式,我们可以通过类比乒乓球盒子来分析。
引用数据类型与堆内存
与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JS不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。 堆存取数据的方式,则与书架与书非常相似。 书虽然也有序的存放在书架上,但是我们只要知道书的名字,我们就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓一样,非得将上面的所有乒乓球拿出来才能取到中间的某一个乒乓球。好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。
为了更好的搞懂栈内存与堆内存,我们可以结合以下例子与图解进行理解。
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] 作为对象存在于堆内存中
因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从栈中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。 理解了JS的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的一些特点了。 在前端面试中我们常常会遇到这样一个类似的题目
-
// demo01.js
-
var a = 20;
-
var b = a;
-
b = 30;
-
// 这时a的值是多少?
-
// demo02.js
-
var m = { a: 10, b: 20 };
-
var n = m;
-
n.a = 15;
-
// 这时m.a的值是多少
在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a
执行之后,a
与b
虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。
在demo02中,我们通过var n = m
执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在栈内存中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在堆内存中访问到的具体对象实际上是同一个。
内存的生命周期
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对象,如临时变量、字符串等; 老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。
请各位老铁see一下以下的代码,来分析一下垃圾回收。
-
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语言的特殊性(闭包...),导致如何判断一个对象是否会被回收的问题上变的异常艰难,各位老铁看看就行。