JS高级 - JS内存管理

104 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的

任意的内存管理都存在如下的生命周期过程:

  1. 分配申请你需要的内存
  2. 使用分配的内存
  3. 不需要使用时,对其进行释放

不同的是某些编程语言需要我们自己手动的管理内存(如C, C++等),

某些编程语言会可以自动帮助我们管理内存(如JAVA, JS, Dart等)

所以JS对于内存的管理是自动的

  • 对于原始数据类型内存的分配会在执行时,直接在栈空间进行分配
    • 虽然最终原始数据类型值会存放在VO对象中,而VO对象是存在于堆内存中
    • 但是对于原始数据类型的分配也就是JS代码的执行依旧是在栈内存中完成的
  • 对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并将这块内存空间的地址(指针,引用)赋值给对应的变量

因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间

在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如malloc函数,free函数

  • 但是这种管理的方式其实非常的低效,影响我们编写逻辑的代码的效率
  • 并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露

所以大部分现代的编程语言都是有自己的垃圾回收机制的

  • 垃圾回收的英文是Garbage Collection,简称GC
  • 对于那些不再使用的对象(也就是没有被引用的那些对象),我们都称之为是垃圾,它需要被回收,以释放更多的内存空间
  • 对于JS而言,栈内存空间一般是不需要进行管理的,因为对应的EC在执行完毕后会自动出栈
  • 所以对于JS而言,GC主要管理的是堆内存中那些复杂数据类型对象

GC算法

为了更好的标识出那些对象是垃圾,什么时候进行内存回收,就需要使用对应GC算法,不同编程语法所使用的GC算法都是不同的

引用计数(Reference counting)

  • 当一个对象有一个引用指向它时,那么这个对象的引用就+1
  • 当一个引用不在指向对应的对象的时候,那么这个对象的引用就-1
  • 当一个对象的引用为0时,说明这个对象就可以被销毁掉

但是引用计数有一个很大的弊端,就是无法清除循环引用

image.png

let obj1 = {}
let obj2 = {}

// 构成循环引用
// 此时obj1所指向的对象的引用计数为2 - obj1 obj2.info
// 同样obj2所指向的对象的引用计数为2 - obj2 obj1.info
obj1.info = obj2
obj2.info = obj1

// 引用移除
obj1 = null
obj2 = null

// 此时obj1所指向的对象和obj2所指向的对象的引用计数都是1,且构成了循环引用
// 此时GC是无法自动移除这两个对象,但如果不做清理,此时就会出现内存泄露
// 解决方法,手动打破该循环引用
obj1.info = null

标记清除(mark-Sweep)

标记清除的核心思路是可达性(Reachability)

设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些 没有引用到的对象,就认为是不可用的对象,这样就可以很好的解决循环引用的问题

一般情况下,根对象是最后才会被移除的对象,并且通过根对象,应该可以遍历到所有被引用的对象

因此一般使用globalThis作为GC的根对象(root object 或者叫RO)

image.png

JS引擎比较广泛的采用的就是可达性中的标记清除算法,当然类似于V8引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法

标记整理(Mark-Compact)

这是一种和“标记-清除”类似的算法

不同的是,回收期间同时会将保留的存储对象搬运汇集到连续的内存空间,从而整合空闲空间,避免内存碎片化

也就是说,标记整理算法会在垃圾对象移除后,整理那些依旧存在的对象,使他们处于连续的内存空间中,从而整合空闲空间,避免内存碎片化

分代收集(Generational collection)

在代码执行的过程中,绝大部分对象在被创建完成后就会立即被使用,使用完毕后就会被销毁

所以没有必要对所有的对象的引用进行查找,我们只需要频繁查找那些新创建的对象

同时应该减少对长期存活的老旧对象的查找频率

因此这种算法将内存划分为了”新生代区域“和”旧生代区域“

新创建的那些对象会存放于新生代区域

同时新生代会被划分为两个内存区域,一个叫做from space,另一个被称之为to space

第一次执行:

  • 所有新创建的对象会被放置到from space中
  • GC在from space中进行遍历后,移除那些垃圾对象
  • 将from space中那些留存下来的对象移动到to space中,并进行标记
  • 此时原本的from space变成to space ,原来的to space变成from space

第二次执行:

  • 重复第一次所执行的操作,将新创建的对象放置到from space中

  • GC遍历from space 移除那些不在被使用的垃圾对象

  • 对于那些从原本的from space中移除过来的对象,如果依旧存在

    也就是经历了两次GC遍历后依旧存在的那些对象就会被移动到旧生代中

    其余留存的对象会被移动到to space中

  • 原本的from space变成to space ,原来的to space变成from space

依次类推 。。。

增量收集(Incremental collection)

如果有许多对象,并且我们试图一次遍历并标记整个对象集

则会花费比挨多的时间,因此会在执行过程中带来比较大的延迟

所以引擎试图将垃圾收集工作分成几部分来做

然后将这几部分会逐一进行处理,这样会有许多微小的延迟而不是一个大的 延迟

闲时收集(Idle-time collection)

垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响

GC内存示意图

image.png

分区说明
old pointer space如果对象的属性依旧是一个对象的时候,这类对象会被存放在这里
old data space如果对象的属性只是基本数据类型的时候,这类对象会被存放在这里
large object space用于存在占用内存比较大的对象
code space进行代码编译和运行的区域