【浏览器工作原理】6. javascript中的数据是如何存储的(内存机制)

407 阅读4分钟

引言

虽然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执行过程中,主要有三种类型内存空间:代码空间、栈空间和堆空间

  1. 代码空间:存储可执行的代码
  2. 栈空间:用来存储执行上下文的,原始类型的数据值都是直接保存在栈中
  3. 堆空间:引用类型的值是存放在堆中的,在栈空间保留对象的引用地址

为什么要用栈和堆两个存储空间呢?

这是因为JavaScript引擎需要用栈来维护程序执行期间上下文的状态,如何栈空间太大会影响上下文的切换效率,进而影响程序的执行效率,使用堆空间可以提升执行效率。

总结:栈空间一般存放原始类型的小数据,堆空间存放引用类型的大数据。 举个例子:


function foo(){
    var a = "极客时间"; // 栈空间
    var b = a; // 栈空间
    var c = {name:"极客时间"}; // 堆空间,栈空间指为堆的地址引用
    var d = c; // 堆空间,和c指向同一个堆空间的地址
}
foo()

执行到第三行时内存存储如下: image.png 执行到第四行时内存存储如下: image.png 执行到第五行时内存存储如下: image.png

垃圾回收

通过上面知道了JavaScript数据是存储在栈和堆内存空间的,那么这两种是如何回收的?

  • 栈内存:javascript引擎会通过向下移动ESP来销毁该函数保存在栈中的执行上下文。

image.png

  • 堆内存:要回收堆中的垃圾数据,需要使用JavaScript中的垃圾回收器。

在v8中会把堆分为新生代和老生代两个区域,因此用两个不同的垃圾回收器进行高效回收。无论什么类型的垃圾回收器,都有一套共同的执行流程:

  1. 标记空间中活动对象(还在使用的对象)和非活动对象(不使用的对象,可以进行回收);

  2. 回收非活动对象所占据的内存

  3. 内存整理(频繁回收对象后,内存中会出现大量不连续空间,需要进一步整理内存,副垃圾回收器不会产生内存碎片)

  4. 副垃圾回收器:负责新生代的垃圾回收 使用scavenge算法:把新生代空间对半划分为两个区域,一半是对象区域(新加入的对象存放),一半是空闲区域

image.png

  • 1.1 对对象区域的垃圾做标记
  • 1.2 进行垃圾清理,把存活的对象复制到空闲区域并进行内存整理
  • 1.3 对象区域与空闲区域进行角色翻转

由于复制大对象会花费比较常时间,为了执行效率,一般新生区的空间会被设置的比较小,很容易被存活的对象占满,因此JavaScript引擎采用了对象晋升策略:经过两次垃圾回收还存活的对象会被移动到老生区中。

  1. 主垃圾回收器:负责老生代的垃圾回收

使用标记-清除/整理算法进行垃圾回收

  • 2.1 标记阶段:从一组根元素开始递归遍历,能到达的元素为活动对象,反之为垃圾数据 image.png
  • 2.2 垃圾清除、整理:清除掉标记为垃圾数据的过程

image.png

image.png

什么时候执行垃圾回收

由于JavaScript是运行在主线程上的,一旦执行垃圾回收算法,JavaScript脚本将暂停,等回收完再恢复。对于新生代影响不大,但是对于老生代可能会造成页面卡顿。

image.png 为了降低卡顿,v8使用了增量标记算法,也就是把垃圾回收任务拆分成很多小任务,穿插在JavaScript任务中间执行。

image.png

总结

通过本文了解了JavaScript中的数据是如何存储及回收的,相信对于我们理解JavaScript执行过程有很大帮助。