JavaScript执行机制之栈内存与堆内存


title: js栈内存与堆内存

前言

关于执行上下文与执行栈、执行过程,已经告一段落。

这一章你会了解到:

  • 三种数据结构: 堆(heap)、栈(stack)、队列(queue)
  • 变量的存放
  • 内存空间管理

注意:栈内存可以理解为当前栈的内存。栈内存与当前执行上下文绑定,仍旧是后进先出。

栈内存与堆内存

队列严格意义上是 JavaScript 中的高级概念“并发模型”,具体运行过程比之单一概念更加复杂。

三种数据结构

JS中三种重要的数据结构, 如图:

img1

(图片来源前端九五六-Javascript 内存空间管理)

栈数据结构

栈的特点: 后进先出(LIFO)的结构.

LIFO: last-in, first-out,类似于向乒乓球桶中放球,最先放入的球最后取出)

这里还是贴上一张网图方便大家理解的好:

img2

栈中的数据就像是一个个乒乓球, 最先进去的最后出来.

注⚠️

这里所说的进栈出栈不是指赋值算进, 使用算出. 而是指赋值算进, 被清理算出, 而且位于同一函数作用域下的变量, 应该是在栈的同一层.

所谓的变量存储于栈内存中的栈,传统意义上说指的是由内存自动创建分配的空间,例如函数的参数值与局部变量,只是其操作方式类似于栈操作,所以叫栈内存。

比如函数调用其实就相当于栈的形式:

例子🌰:

function fn1() {
	console.log(1)
  fn2()
}
function fn2() {
	console.log(2)
	fn3()
}
function fn3() {
	console.log(3)
}
fn1()

如上, 声明的顺序是1, 2, 3 , 但是释放的顺序是为3, 2, 1 .

这里释放按照这个顺序是因为 3最先执行完, 所以最先被释放.

堆数据结构

一种树状结构。好比 JSON 格式中的数据,你有 key,我有对应的 value, 就立马返给你。

因为我们知道JSON格式的存储是无序的, 所以没有先后顺序, 所以它是一种绝对公平的数据结构。

注意:引用类型都会开辟堆内存。同时v8特性,有this指向堆内存保持着引用,该内存就不会释放。

如图所示:

img3

队列数据结构

队列数据结构不同于堆, 队列是一种 先进先出(FIFO) 的数据结构.

它也是 事件循环(Event Loop) 的基础结构.

如图所示:

img4

最先进入队列的任务最先出来, 类似于排队买菜, 排在前面的人先买,并且买完所有菜。才能轮到下一个人(事件)。

队列的函数处理会一直进行到执行栈再次为空为止,然后事件循环将会处理队列中的下一个消息(如果还有的话)。

对于 队列(事件循环) ,展开来说还有微任务与宏任务。微任务对应执行栈,宏任务对应队列。

变量对不同内存的引用

变量的存放

通过上面的介绍我们知道了, 内存中有堆了栈, 那么JS变量具体是存放在哪里呢?

  • 基本数据类型保存在 栈内存,与该内存与执行上下文绑定,出栈后自动销毁;
  • 引用数据类型保存在 堆内存,变量保存了 this 引用,与堆内存绑定,需要 V8 垃圾回收.
  1. 基本数据类型6种: Undefined、Null、Boolean、Number、String、Symbol, 由于他们在内存中分别占有固定大小的空间, 通过按值来访问.
  2. 引用数据类型: 也就是Object对象, 它的存储分为访问地址实际存放的地方; 访问地址是存储在中的, 当查询引用类型变量的时候, 会先从中读取内存地址(也就是访问地址), 然后再通过地址找到中的值.因此, 这种我们也把它叫为引用访问.

一张图方便你理解🤔

img5

在计算机的数据结构中,栈比堆的运算速度快,Object是一个复杂的结构且可以扩展:数组可扩充,对象可添加属性,都可以增删改查。将他们放在堆中是为了不影响栈的效率。而是通过引用的方式查找到堆中的实际对象再进行操作。所以查找引用类型值的时候先去查找再去查找。

变量存放案例

要是你读完了上面的堆栈存储介绍还有点模糊的话, 我们不妨来看几个案例.

案例一🌰:

var a = 1;
var b = a;
b = 2;
console.log(a); // a = 1

案例二🌰:

var obj1 = { a: 1, b: 2 };
var obj2 = obj1;
obj2.a = 3;
console.log(obj1.a); // obj1.a = 3
// 变量保存引用类型,只能保存到一个引用地址,变量与堆内存不直接绑定!
// obj1 和 obj2 都保存了同一个引用地址,指向同一个堆内存,所以堆内存改变会一起改变

案例三🌰:

var obj1 = { a: 1 };
var obj2 = obj1;
obj1 = null;
console.log(obj2); // obj2 = { a: 1 }
// 虽然前面 obj1、obj2 都保存了堆内存地址,但后面只有 obj1 把保存的值改成null,所以并不影响 obj2 保存的引用地址指向堆内存。

内存空间管理

在上面我们说了那么多的栈内存, 堆内存, 那么在JS中, 是怎样管理这些内存空间的呢?

首先, 同样的, 内存空间也是有属于自己的生命周期, 它主要分为三个阶段:

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

我们可以用个例子来看一下看.

案例一🌰:

var a = 1; // 在内存中给数值变量分配空间
alart(a + 2); // 使用内存
a = null; // 使用完后, 释放内存空间

上面三步分别对应着三个阶段. 当然, a = null这个操作是我们手动将a的内存空间释放. 若没有这个过程, JS 的垃圾回收机制,也会帮助开发者自动做释放内存的工作。

垃圾收集器 会找出那些不再有引用的值,然后释放其占用的内存。会每隔固定的时间段就执行一次释放操作。

在自动垃圾收集机制中, 最常用的就是通过 标记清除 的算法来找到那些不再继续使用的对象。

使用 a = null 就是做了一个释放引用的操作, 让 a 原本对应的值失去引用, 脱离执行环境。这个值就会在当前执行上下文出栈后,下一次垃圾收集器执行操作时被找到,并被释放.

但这也只是变量处于局部执行上下文才容易释放,对于全局执行上下文,因为在整个应用的生命周期中从一打开就处于整个执行栈的最底层,所以难以释放。

除了变量全局污染,对于垃圾回收的不利,也是少用全局变量的重要原因。

总结

栈内存(stack):栈内存是当前函数作用域的内存,与当前执行上下文绑定。

堆内存(heap):堆内存是区别于栈区、代码区,是独立的另一个内存区域。无法直接赋值给变量,JavaScript 变量的赋值操作,只能引用其内存地址。

队列(queue):事件队列是一种并发模型,当存在多个异步事件,需要队列来调度事件任务的入栈顺序。

垃圾回收:栈内存在出栈时,会直接释放。堆内存依赖垃圾收集器每隔一段时间收集标记,判断是否可以释放,只要有变量引用,就不会被回收。对于复杂场景,开发时需注意释放变量。

参考文章:

木易杨前端进阶-JavaScript深入之内存空间详细图解

前端基础进阶(一):内存空间详细图解

前端九五六-Javascript 内存空间管理

关于js中 “栈空间的先进后出,后进先出” 的疑问?

霖呆呆的blog

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值