调用堆栈以及js的内存回收机制

2,206 阅读6分钟

相关术语解释:

调用栈: 具有LIFO(先进后出)结构。用于存储在代码执行期间创建的所有执行上下文
执行栈,解释器(就像浏览器的JavaScript解释器)追踪函数执行流的一种机制。通过此机制,追踪到哪个函数正在执行,执行的函数体中又调用了哪些函数

  1. 每调用一个函数,解释器就会把函数添加进调用栈并开始执行
  2. 正在调用栈中执行的函数还调用了其他函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行
  3. 当前函数执行完毕后,解释器将其清除调用栈,继续执行当前执行环境下的剩余代码。
  4. 当分配的调用栈空间被占满时,会也引发“堆栈溢出”

一开始,调用栈为空,直到函数被调用,便自动地添加进调用栈,执行完函数体中的代码后,调用栈又会自动地移除这个函数。这样依次类推,直到调用栈又为空。


执行上下文和执行栈

  1. 执行上下文的类型

    • 全局执行上下文: 只有一个。浏览器中的全局对象就是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、在代码执行阶段,会再次修改变量对象的属性值


变量的存放

  1. 基本类型: 保存在栈中,占有固定大小的空间。(Undefined、Null、Boolean、String和Symbol)
  2. 引用类型:栈存放该对象的访问地址,内存存储在堆内存中,不限大小。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。

在计算机的数据结构中,栈比堆的运算速度快

js内存机制

js内存空间分为栈、堆、池(一般也会归类为栈中)。其中栈存放变量, 堆存放复杂对象、池存放常量,所以也叫常量池

闭包中的变量并不保存在栈内存中,而是保存在堆内存中。这也是函数之后为什么闭包还能引用到函数内的变量


内存回收

JavaScript有自动垃圾收集机制,垃圾收集器会每隔一段时间就执行一次释放操作,展出那些不在继续使用的值,然后释放其占用的内存。

局部变量和全局变量的销毁:

  • 局部变量: 局部变量的生存依赖于其函数执行上下文环境中,函数执行完毕后,其局部变量也很容易被垃圾收集器作出判断并回收。
  • 全局变量: 对于垃圾收集器而言,全局变量什么时候要回收是很难判断的。所以尽量少用全局变量,并且用完最好将其清空内存。

垃圾回收算法

核心思想: 如何判断内存已经不再使用。

常用算法有两种:

  1. 引用计数(现代浏览器不再使用)
  2. 标记清除(常用)

引用计数:判断一个对象是否有指向它的引用。如果没有则回收

但是有一个致命问题:循环引用

两个对象互相引用的话,垃圾回收器就不会进行回收,最终可能导致内存泄露。 这也是现代浏览器不再使用这个算法的原因。 (但IE依旧使用)

标记清除(常用)

对象:“不再使用的对象”定义为“无法到达的对象

即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。

所以现在对主流浏览器来说,只需要切断需要回收的对象与根部的联系。

内存泄露

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

内存泄露识别方法

  1. 浏览器设置

    1. 打开开发者工具,选择 Memory
    2. 在右侧的Select profiling type字段里面勾选 timeline点击左上角的录制按钮。
    3. 在页面上进行各种操作,模拟用户的使用情况。
    4. 一段时间后,点击左上角的 stop 按钮,面板上就会显示这段时间的内存占用情况。
  2. 命令行方法

使用node提供的process.memoryUsage方法

console.log(process.memoryUsage());

// 输出
{ 
  rss: 27709440,		// resident set size,所有内存占用,包括指令区和堆栈
  heapTotal: 5685248,   // "堆"占用的内存,包括用到的和没用到的
  heapUsed: 3449392,	// 用到的堆的部分
  external: 8772 		// V8 引擎内部的 C++ 对象占用的内存
}

判断内存泄漏,以heapUsed字段为准。

  1. weakMap

ES6 新出的两种数据结构:WeakSet 和 WeakMap,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。


常见的内存泄露

  1. 四种常见的js内存泄露

    1. 意外的全局变量
    function foo() {
    this.variable = "potential accidental global";
    }
    
    // Foo 调用自己,this 指向了全局对象(window)
    // 而不是 undefined
    foo();
    

    解决方法:

    在 JavaScript 文件头部加上 'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。

  2. 被遗忘的计时器或回调函数

  3. 脱离 DOM 的引用

  4. 闭包

文章来源:木易杨前端进阶:www.muyiy.cn/blog/1/1.1.…