持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情
不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的
任意的内存管理都存在如下的生命周期过程:
- 分配申请你需要的内存
- 使用分配的内存
- 不需要使用时,对其进行释放
不同的是某些编程语言需要我们自己手动的管理内存(如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时,说明这个对象就可以被销毁掉
但是引用计数有一个很大的弊端,就是无法清除循环引用
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)
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内存示意图
| 分区 | 说明 |
|---|---|
| old pointer space | 如果对象的属性依旧是一个对象的时候,这类对象会被存放在这里 |
| old data space | 如果对象的属性只是基本数据类型的时候,这类对象会被存放在这里 |
| large object space | 用于存在占用内存比较大的对象 |
| code space | 进行代码编译和运行的区域 |