引言
虽然JavaScript不需要我们管理内存,但是我们如果了解数据在内存中的存储方式是非常有必要的。看个简单的例子
function foo(){
var a = 1
var b = a
var c = {name: 'xiaoming'};
a = 2
var d = c;
c.name='zhangsan';
console.log(a); //2
console.log(b); // 1
console.log(d.name); // zhangsan
}
foo()
上面例子为什么修改基本类型和修改引用类型结果与预期不一致呢?要想彻底弄清楚这个问题,就需要理解JavaScript运行过程中数据是如何存储的。
内存机制
在JavaScript执行过程中,主要有三种类型内存空间:代码空间、栈空间和堆空间
。
- 代码空间:存储可执行的代码
- 栈空间:用来存储执行上下文的,原始类型的数据值都是直接保存在栈中
- 堆空间:引用类型的值是存放在堆中的,在栈空间保留对象的引用地址
为什么要用栈和堆两个存储空间呢?
这是因为JavaScript引擎需要用栈来维护程序执行期间上下文的状态,如何栈空间太大会影响上下文的切换效率,进而影响程序的执行效率,使用堆空间可以提升执行效率。
总结
:栈空间一般存放原始类型的小数据,堆空间存放引用类型的大数据。
举个例子:
function foo(){
var a = "极客时间"; // 栈空间
var b = a; // 栈空间
var c = {name:"极客时间"}; // 堆空间,栈空间指为堆的地址引用
var d = c; // 堆空间,和c指向同一个堆空间的地址
}
foo()
执行到第三行时内存存储如下: 执行到第四行时内存存储如下: 执行到第五行时内存存储如下:
垃圾回收
通过上面知道了JavaScript数据是存储在栈和堆内存空间的,那么这两种是如何回收的?
- 栈内存:javascript引擎会通过向下移动ESP来销毁该函数保存在栈中的执行上下文。
- 堆内存:要回收堆中的垃圾数据,需要使用JavaScript中的垃圾回收器。
在v8中会把堆分为新生代和老生代两个区域,因此用两个不同的垃圾回收器进行高效回收。无论什么类型的垃圾回收器,都有一套共同的执行流程:
-
标记空间中活动对象(还在使用的对象)和非活动对象(不使用的对象,可以进行回收);
-
回收非活动对象所占据的内存
-
内存整理(频繁回收对象后,内存中会出现大量不连续空间,需要进一步整理内存,副垃圾回收器不会产生内存碎片)
-
副垃圾回收器:负责新生代的垃圾回收 使用scavenge算法:把新生代空间对半划分为两个区域,一半是对象区域(新加入的对象存放),一半是空闲区域
- 1.1 对对象区域的垃圾做标记
- 1.2 进行垃圾清理,把存活的对象复制到空闲区域并进行内存整理
- 1.3 对象区域与空闲区域进行角色翻转
由于复制大对象会花费比较常时间,为了执行效率,一般新生区的空间会被设置的比较小,很容易被存活的对象占满,因此JavaScript引擎采用了对象晋升策略:经过两次垃圾回收还存活的对象会被移动到老生区中。
- 主垃圾回收器:负责老生代的垃圾回收
使用标记-清除/整理算法进行垃圾回收
- 2.1 标记阶段:从一组根元素开始递归遍历,能到达的元素为活动对象,反之为垃圾数据
- 2.2 垃圾清除、整理:清除掉标记为垃圾数据的过程
什么时候执行垃圾回收
由于JavaScript是运行在主线程上的,一旦执行垃圾回收算法,JavaScript脚本将暂停,等回收完再恢复。对于新生代影响不大,但是对于老生代可能会造成页面卡顿。
为了降低卡顿,v8使用了增量标记算法,也就是把垃圾回收任务拆分成很多小任务,穿插在JavaScript任务中间执行。
总结
通过本文了解了JavaScript中的数据是如何存储及回收的,相信对于我们理解JavaScript执行过程有很大帮助。