title: js栈内存与堆内存
前言
关于执行上下文与执行栈、执行过程,已经告一段落。
这一章你会了解到:
- 三种数据结构: 堆(heap)、栈(stack)、队列(queue)
- 变量的存放
- 内存空间管理
注意:栈内存可以理解为当前栈的内存。栈内存与当前执行上下文绑定,仍旧是后进先出。
队列严格意义上是 JavaScript 中的高级概念“并发模型”,具体运行过程比之单一概念更加复杂。
三种数据结构
JS
中三种重要的数据结构, 如图:
(图片来源前端九五六-Javascript 内存空间管理)
栈数据结构
栈的特点: 后进先出(LIFO)的结构.
(LIFO
: last-in, first-out
,类似于向乒乓球桶中放球,最先放入的球最后取出)
这里还是贴上一张网图方便大家理解的好:
栈中的数据就像是一个个乒乓球, 最先进去的最后出来.
注⚠️
这里所说的进栈和出栈不是指赋值算进, 使用算出. 而是指赋值算进, 被清理算出, 而且位于同一函数作用域下的变量, 应该是在栈的同一层.
所谓的变量存储于栈内存中的栈,传统意义上说指的是由内存自动创建分配的空间,例如函数的参数值与局部变量,只是其操作方式类似于栈操作,所以叫栈内存。
比如函数调用其实就相当于栈的形式:
例子🌰:
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](https://i-blog.csdnimg.cn/blog_migrate/57565e1ccac13d91380ba861e74e8c61.png)
队列数据结构
队列数据结构不同于堆, 队列是一种 先进先出(FIFO) 的数据结构.
它也是 事件循环(Event Loop) 的基础结构.
如图所示:
![img4](https://i-blog.csdnimg.cn/blog_migrate/20381de7c959fad8e92d8212e5e31304.png)
最先进入队列的任务最先出来, 类似于排队买菜, 排在前面的人先买,并且买完所有菜。才能轮到下一个人(事件)。
队列的函数处理会一直进行到执行栈再次为空为止,然后事件循环将会处理队列中的下一个消息(如果还有的话)。
对于 队列(事件循环) ,展开来说还有微任务与宏任务。微任务对应执行栈,宏任务对应队列。
变量对不同内存的引用
变量的存放
通过上面的介绍我们知道了, 内存中有堆了栈, 那么JS
变量具体是存放在哪里呢?
- 基本数据类型保存在 栈内存,与该内存与执行上下文绑定,出栈后自动销毁;
- 引用数据类型保存在 堆内存,变量保存了 this 引用,与堆内存绑定,需要 V8 垃圾回收.
- 基本数据类型6种:
Undefined、Null、Boolean、Number、String、Symbol
, 由于他们在内存中分别占有固定大小的空间, 通过按值来访问. - 引用数据类型: 也就是
Object
对象, 它的存储分为访问地址和实际存放的地方; 访问地址是存储在栈中的, 当查询引用类型变量的时候, 会先从栈中读取内存地址(也就是访问地址), 然后再通过地址找到堆中的值.因此, 这种我们也把它叫为引用访问.
一张图方便你理解🤔
![img5](https://i-blog.csdnimg.cn/blog_migrate/900030e7c2510c30e7d93ad883c6d44c.png)
在计算机的数据结构中,栈比堆的运算速度快,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
中, 是怎样管理这些内存空间的呢?
首先, 同样的, 内存空间也是有属于自己的生命周期, 它主要分为三个阶段:
- 分配你所需的内存;
- 使用分配到的内存(读、写);
- 不需要的时候将其释放、归还.
我们可以用个例子来看一下看.
案例一🌰:
var a = 1; // 在内存中给数值变量分配空间
alart(a + 2); // 使用内存
a = null; // 使用完后, 释放内存空间
上面三步分别对应着三个阶段. 当然, a = null
这个操作是我们手动将a
的内存空间释放. 若没有这个过程, JS
的垃圾回收机制,也会帮助开发者自动做释放内存的工作。
垃圾收集器 会找出那些不再有引用的值,然后释放其占用的内存。会每隔固定的时间段就执行一次释放操作。
在自动垃圾收集机制中, 最常用的就是通过 标记清除 的算法来找到那些不再继续使用的对象。
使用 a = null
就是做了一个释放引用的操作, 让 a
原本对应的值失去引用, 脱离执行环境。这个值就会在当前执行上下文出栈后,下一次垃圾收集器执行操作时被找到,并被释放.
但这也只是变量处于局部执行上下文才容易释放,对于全局执行上下文,因为在整个应用的生命周期中从一打开就处于整个执行栈的最底层,所以难以释放。
除了变量全局污染,对于垃圾回收的不利,也是少用全局变量的重要原因。
总结
栈内存(stack):栈内存是当前函数作用域的内存,与当前执行上下文绑定。
堆内存(heap):堆内存是区别于栈区、代码区,是独立的另一个内存区域。无法直接赋值给变量,JavaScript 变量的赋值操作,只能引用其内存地址。
队列(queue):事件队列是一种并发模型,当存在多个异步事件,需要队列来调度事件任务的入栈顺序。
垃圾回收:栈内存在出栈时,会直接释放。堆内存依赖垃圾收集器每隔一段时间收集标记,判断是否可以释放,只要有变量引用,就不会被回收。对于复杂场景,开发时需注意释放变量。
参考文章: