JS垃圾回收机制&内存管理

本文探讨了JavaScript的学习动机,特别是内存管理和性能优化,介绍了抽象语法树(AST)、V8引擎的工作原理,包括其编译流程和垃圾回收机制,重点讲解了标记清除法、新生代和老生代的内存管理策略以及并发回收等技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面

为什么要学习JS运行机制?

  • 我们要保证一个程序在运行过程中的性能,就要保证它的内存回收和分配。(JS中不需要人为地去做内存分配)

  • 在程序执行后,要保证没有遗留下的,没法被自动回收的内存空间(JS引擎会自动移除不再使用的内容)

  • 所以我们学习JS运行机制,是为了合理地分配和回收内存,并且减少内存占用,确保运行时的性能。

AST(抽象语法树)

JS是一种解释型语言,也就是每次运行,都要将源代码(字符串)编译成机器码。

学过编译原理的都知道,源代码会经过 词法分析 -> 语法分析 -> 语义分析 -> 代码生成 这一流程,最终被编译成机器码。

而我们的JS引擎提供了对应的能力,源代码经过 Lexer 进行词法分析,通过 Parser 被转换成抽象语法树 AST,这一步也就是上述语法分析的过程,然后交给 Interpreter 进行后续的编译处理。

很多第三方库都有代码的转换,分成Parser, Transformer, Generator,分别对应分析、转换、生成的功能

JS引擎

JavaScript 其实有众多引擎,只不过 v8 是我们最为熟知的。

  • V8 (Google),用 C++编写,开放源代码,由 Google 丹麦开发,是 Google Chrome 的一部分,也用于 Node.js。

  • JavaScriptCore (Apple),开放源代码,用于 webkit 型浏览器,如 Safari ,2008 年实现了编译器和字节码解释器,升级为了 SquirrelFish。苹果内部代号为“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。

  • Rhino,由 Mozilla 基金会管理,开放源代码,完全以 Java 编写,用于 HTMLUnit

  • SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,现时用于 Mozilla Firefox。

举个例子,sort()方法在各个JS引擎中的实现是不同的:

浏览器

JS引擎

排序算法

Google Chrome

V8

插入排序和快速排序

Mozilla Firefox

SpiderMonkey

归并排序

Safari

Nitro

归并排序和桶排序

Microsoft Edge 和 IE(9+)

Chakra

快速排序

接下去着重讲V8,V8引擎编译时的流程如下:

  1. 通过 Parser 转换器将JS代码转换为抽象语法树(AST)

  2. 使用 Ignition 解释器生成字节码(Byte code)

  3. 执行代码

  4. 通过TurboFan,V8引擎可以对频繁使用的代码进行反馈,对代码进行优化

  5. 最终产出优化后的代码

垃圾回收

我们的 JS 自带GC,因此相对来说程序员对于内存管理是无感的。

我们写代码无论是定义基本类型,还是引用类型,都需要占用内存,但是我们又不需要像c语言那样手动去执行malloc分配内存,这就是因为我们的JS引擎提供了内存分配的能力。

那么 JS 引擎的垃圾回收到底是什么样的机制呢?

可达性:有没有变量去获取到对应的地址空间中的内容,我们要清除的就是不可达的数据

引用计数法

变量被定义时,如果一个引用类型被赋给了该变量,那么这个值的引用次数就是1。

如果这个值又被赋给了其他变量,那么引用数加1。

如果变量的值被其他值覆盖了,那么引用次数减1。

如果一个值的引用数为0时,说明这个值不会再访问了,GC会在运行时清除内存中的内容。

现在基本上不会用引用计数法来做垃圾回收的依据 ,因为如果遇到循环引用,或者不恰当的闭包,那么永远也无法清除这部分的内存。

function fn() {
  let A = new Object(); // 值1
  let B = new Object(); // 值2
  
  A.b = B;
  B.a = A;
  // 此时 值1 的引用数为 1+1=2
  // 此时 值2 的引用数为 1+1=2
  
  A = null;
  B = null;
  // 此时 值1 的引用数为 2-1=1
  // 此时 值2 的引用数为 2-1=1 
}

此外,计数器也是需要额外开辟地址空间的。

标记清除法

标记清除法定义了一个根对象,在JS中根对象就是全局对象。GC会定期从根对象开始,找所有从根开始引用的对象,然后找这些对象引用的对象。所以标记清除法就是从根开始,找到所有可达的对象,收集所有不可达的对象。

标记清除法是 JS 引擎中最常用到的垃圾回收算法,不同浏览器的JS引擎对此算法有不同的优化,且GC运行的频率会有所不同。

标记清除法的流程如下:

  1. 运行时,内存中所有对象都被标记为0,假定他们不再被使用。

  2. 从根对象开始,找到被引用的对象,将他们置1。

  3. 清除所有被标记为0的对象,销毁并释放内存空间。

  4. 重新将所有对象置0,等待下一轮标记清除。

缺点:

内存空间虽然被释放,但是空闲的空间可能不连续,因为剩余的空间位置是不变的,所以会产生很多内存碎片。

解决方法:

  • 首次适应算法(First-fit):从空闲分区表的第一项开始查找,找到第一个能满足要求的分区分配给作业。

  • 最佳适应算法(Best-fit):遍历整个空闲分区表,找到满足作业要求的最小分区分配给作业。

  • 最坏适应算法(Worst-fit):遍历整个空闲分区表,找到最大分区分配给作业。

这三种策略里面 Worst-fit 的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用。First-fit 对于分配的效率来说是最好的选择。

标记整理法

标记整理法可以有效解决内存碎片的问题,标记结束后,会将所有可达的对象向内存的一端移动。

内存管理

V8引擎将堆内存分为新生代和老生代两个区域,对这两部分采用不同的策略进行垃圾回收。

新生代

新生代又分为空闲区和使用区。

新创建的对象会被加入到使用区,当使用区快要被写满时,新生代垃圾回收器就会对使用区中的对象进行标记清除,标记为1的对象将会被复制到空闲区并进行排序。最后进行翻转,将原来的空闲区变为使用区,使用区变为空闲区。

如果一个对象经过多次复制后依然存活,那么它将被认为是生命周期较长的对象,且会被移动到老生代中进行管理。除此之外,还有一种情况,如果复制一个对象到空闲区时,空闲区的空间占用超过了25%,那么这个对象会被直接晋升到老生代空间中。

老生代

老生代中存储的是使用相对频繁并且短时间无需回收的内容,这部分会使用到标记整理法进行处理。

并行回收

为了提高GC效率,使用辅助进程。

全停顿标记

就是阻塞

切片标记

将一次标记拆分为多个部分,每个部分之间让应用运行一段时间,经过一段时间的交替最终完成标记。

三色标记

  • 白色表示未被标记的对象

  • 灰色表示自身被标记,但成员变量未被标记

  • 黑色表示自身和成员变量都被标记

写屏障

依赖三色标记,在增量标记时修改引用的处理

惰性清理

V8引擎使用惰性清理策略,当增量标记完成后,如果可用内存支持代码的继续执行,那么就没必要立即执行清除,V8引擎会逐一对不可达的对象进行清理。

并发回收

并发回收是更进一步的切片,几乎完全不阻塞主进程。

内存泄漏

面试常问

  1. 怎么理解内存泄漏

  2. 怎么解决内存泄漏,代码层面如何优化?

原因

  • 意外的全局变量

  • 不恰当的闭包使用

  • 没有及时销毁的定时器和事件

避免方法

  • 减少查找

var i, str = ""
function packageDomGlobal() {
    for(i = 0; i < 1000; i++) {
        str += i
    }
}

// 第二种情况。我们采用局部变量来保存保存相关数据
function packageDomLocal() {
    let str = ''
    for(let i = 0; i < 1000; i++) {
        str += i
    }
}
  • 减少变量声明

// 第一种情况,循环体中没有抽离出值不变的数据
var test = () => {
  let arr = ['czs', 25, 'I love FrontEnd'];
  for(let i = 0; i < arr.length; i++){
      console.log(arr[i]);
  }
}

// 第二种情况,循环体中抽离出值不变的数据
var test = () => {
  let arr = ['czs', 25, 'I love FrontEnd'];
  const length = arr.length;
  for(let i = 0; i < length; i++){
      console.log(arr[i]);
  }
}
  • 使用 Performance + Memory 分析内存与性能

  • setTimeout、setInterval 需要及时清除

  • 事件绑定的清除

定位内存泄漏

我们可以利用Google Devtool去定位网页内存泄漏问题

Performance

录制屏幕

打开Memory,去看HEAP中的曲线,可以定位到哪次交互事件导致内存占用升高。

Memory

分为堆快照、时间轴上的分配插桩、分配采样三种模式。

其中堆快照可以识别DOM中导致内存泄漏的原因,通过对比查看交互事件前后内存占用情况。

分配时间轴可以可视化地记录内存使用情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值