调用堆栈以及js的内存回收机制
相关术语解释:
调用栈: 具有LIFO(先进后出)结构。用于存储在代码执行期间创建的所有执行上下文 执行栈,解释器(就像浏览器的JavaScript解释器)追踪函数执行流的一种机制。通过此机制,追踪到哪个函数正在执行,执行的函数体中又调用了哪些函数
- 每调用一个函数,解释器就会把函数添加进调用栈并开始执行
- 正在调用栈中执行的函数还调用了其他函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行
- 当前函数执行完毕后,解释器将其清除调用栈,继续执行当前执行环境下的剩余代码。
- 当分配的调用栈空间被占满时,会也引发“堆栈溢出”
一开始,调用栈为空,直到函数被调用,便自动地添加进调用栈,执行完函数体中的代码后,调用栈又会自动地移除这个函数。这样依次类推,直到调用栈又为空。
执行上下文和执行栈
-
执行上下文的类型
- 全局执行上下文: 只有一个。浏览器中的全局对象就是window对象,this指向这个全局对象。
- 函数执行上下文: 存在无数个,只有函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
- eval函数执行上下文:指的是运行在eval函数中的代码,很少用且不建议使用
执行上下文的创建:
1. 创建阶段
2. 执行阶段
变量对象
声明优先级:函数声明优先级高于变量声明 同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明
执行上下文栈
因为JS引擎创建了很多的执行上下文,所以JS引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
函数上下文
在函数上下文中,用活动对象(activation object, AO)来表示变量对象。
活动对象和变量对象的区别在于:
1、变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。 2、当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。
总结如下:
1、全局上下文的变量对象初始化是全局对象
2、函数上下文的变量对象初始化只包括 Arguments 对象
3、在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
4、在代码执行阶段,会再次修改变量对象的属性值
变量的存放
- 基本类型: 保存在栈中,占有固定大小的空间。(Undefined、Null、Boolean、String和Symbol)
- 引用类型:栈存放该对象的访问地址,内存存储在堆内存中,不限大小。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。
在计算机的数据结构中,栈比堆的运算速度快
js内存机制
js内存空间分为栈、堆、池(一般也会归类为栈中)。其中栈存放变量, 堆存放复杂对象、池存放常量,所以也叫常量池
闭包中的变量并不保存在栈内存中,而是保存在堆内存中。这也是函数之后为什么闭包还能引用到函数内的变量
内存回收
JavaScript有自动垃圾收集机制,垃圾收集器会每隔一段时间就执行一次释放操作,展出那些不在继续使用的值,然后释放其占用的内存。
局部变量和全局变量的销毁:
- 局部变量: 局部变量的生存依赖于其函数执行上下文环境中,函数执行完毕后,其局部变量也很容易被垃圾收集器作出判断并回收。
- 全局变量: 对于垃圾收集器而言,全局变量什么时候要回收是很难判断的。所以尽量少用全局变量,并且用完最好将其清空内存。
垃圾回收算法
核心思想: 如何判断内存已经不再使用。
常用算法有两种:
- 引用计数(现代浏览器不再使用)
- 标记清除(常用)
引用计数:判断一个对象是否有指向它的引用。如果没有则回收
但是有一个致命问题:循环引用。
两个对象互相引用的话,垃圾回收器就不会进行回收,最终可能导致内存泄露。 这也是现代浏览器不再使用这个算法的原因。 (但IE依旧使用)
标记清除(常用)
对象:“不再使用的对象”定义为“无法到达的对象”
即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。
所以现在对主流浏览器来说,只需要切断需要回收的对象与根部的联系。
内存泄露
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)
内存泄露识别方法
-
浏览器设置:
- 打开开发者工具,选择 Memory
- 在右侧的Select profiling type字段里面勾选 timeline点击左上角的录制按钮。
- 在页面上进行各种操作,模拟用户的使用情况。
- 一段时间后,点击左上角的 stop 按钮,面板上就会显示这段时间的内存占用情况。
-
命令行方法
使用node提供的process.memoryUsage
方法
console.log(process.memoryUsage());
// 输出
{
rss: 27709440, // resident set size,所有内存占用,包括指令区和堆栈
heapTotal: 5685248, // "堆"占用的内存,包括用到的和没用到的
heapUsed: 3449392, // 用到的堆的部分
external: 8772 // V8 引擎内部的 C++ 对象占用的内存
}
判断内存泄漏,以heapUsed字段为准。
- weakMap
ES6 新出的两种数据结构:WeakSet 和 WeakMap,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。
常见的内存泄露
-
四种常见的js内存泄露
- 意外的全局变量
function foo() { this.variable = "potential accidental global"; } // Foo 调用自己,this 指向了全局对象(window) // 而不是 undefined foo();
解决方法:
在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。
-
被遗忘的计时器或回调函数
-
脱离 DOM 的引用
-
闭包
文章来源:木易杨前端进阶:www.muyiy.cn/blog/1/1.1.…
转载自:https://juejin.cn/post/6844903984495329288