文章目录
JavaScript内嵌了垃圾回收器
1. 内存周期
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放、归还
2. 内存分配
在 JavaScript 中,内存一般分为栈内存和堆内存。基本类型储存在栈内存,栈内存由操作系统管理;引用类型储存在堆内存,堆内存由引擎管理。
3. 内存的回收和释放
常说的内存管理是在堆内存上的。内存的回收,也就是垃圾回收机制是有算法(策略)的。最常见的有两种,一种是引用计数,还有一个是标记-清除。
3.1 引用计数
最早期的垃圾回收策略是引用计数。
思路:对每个值都记录它的引用次数。声明变量并引用时,这个值得引用数就会加1,类似地,如果对值引用的变量被其他值覆盖了,那么引用数就减1。垃圾回收程序下次运行就会释放引用数为0的内存。
引用计数有一个严重的问题:循环引用,就是对象A有一个指针指向对象B,而对象B也引用了对象A。它们永远不会被回收。这就会造成内存泄漏
早期的 IE 浏览器使用的就是引用计数
优点:
立即回收。当一个内存的引用为0的时候,这部分的内存会被立即回收。减少程序的暂停。当前执行平台的内存肯定是有上限的,所以内存肯定有占满的时候。由于引用计数算法是时刻监控着内存引用值为0的对象,保证了当前内存是不会有占满的时候。
缺点:
循环引用对象无法被回收。按照引用计数的回收标准,函数内部的循环引用的对象是没法被回收的,因为他们的引用数永远不会是0。计数比较耗时。需要频繁的去查看哪些对象引用数为0了,当维护的对象数大的时候,查找的过程就比较耗时了。引用计数的GC会占用主线程,会阻塞其他任务的运行。
3.2 标记-清除
这个策略分为标记和清除两个阶段。标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
可达性:就是可以到达,因为堆内存中往往都存在根对象(不是传统意义上的对象)。我们标记也是从根对象上去开始递归遍历的。当一个访问一个对象的路径内切断了,他就是不可达的。那么就需要被清理。
所以,当函数执行完毕的时候,当前函数就和全局断开了连接。这就是我们一般是没法访问函数内部定义的变量的原因(特殊的闭包除外)。这时候这些变量就会因为没法到达而无法标记,所以就会在下次的 GC 的时候被回收。
标记清除和引用计数的最大区别就是,回收的标准的不同。零引用的内存一定不可到达,但是非零引用的内存不一定可达到。
标记清除算法大致流程:
- 垃圾收集器给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点:
实现简单。打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记。
缺点:
内存碎片化。在回收内存后,原来对象占用的位置被空下来,这就造成了内存的不完整性。

分配速度慢。将这些碎片化的内存重新分配是需要时间的,一般采用以下方案:First-fit,就是找到大于或者等于需要分配内存大小的内存后,就立刻返回。(常用)Best-fit,查找整个内存,直到找到符合待分配大小的最小的内存块。Worst-fit,查找整个内存,找到最大的内存块,先切除需要分配大小的内存部分,然后返回剩下的。

标记整理:用来解决回收内存后,内存的不完整性问题。原理其实就是在标记结束后多干一件事情,将活着的对象向内存的一端移动,最后清理掉边界的内存。

4. V8 引擎的垃圾回收机制
V8 是一个由 Google 开源的高性能 JavaScript引擎,其源代码使用 C++ 编写。V8 被用于 Google 的开源浏览器 Chrome 中,同时也被用于 Node.js,以及其他一些软件中。
V8 的垃圾回收也是基于标记清除算法
4.1 分代式垃圾回收
将堆内存分为新生代和老生代两个区域,然后两个区域采用不同的垃圾回收策略。
新生代:生命周期短,占用内存小
老生代:生命周期长或者占用大小比较大。

新生代
新生代的对象一般是存活时间比较短且比较小的对象。这部分通常只有很小的内存分配,一般是 1~8M 的容量。
Cheney算法将新生代中的内存一分为二,一个是出于使用状态的区域,我们称之为 使用区,一个是出于闲置状态的 称之为空闲区。
流程:首先先进入标记流程,新生代垃圾回收器找出使用区内的活动的对象进行标记,接着就是将被标记的对象复制到空闲区并排序。随后,进入垃圾清洁阶段,将使用区中的未被标记的空间清理掉,最后将两个区域角色互换一下。

晋升规则:
- 在一个变量的移动阶段,如果这个变量所占用的空间的大小
超过了空闲区的25%,那么这个变量会直接晋升为老生代。 - 当一个变量已经进过了
多次交换后还存活,那么这个变量也会晋升为老生代。
老生代
主要使用了标记-清除算法。在此基础上,使用了许多优化技术。
全量清除:JavaScript 是一门单线程的语言,他运行在主线程上,垃圾回收的时候势必会 阻塞 JavaScript 脚本的执行,等垃圾回收完毕后再恢复脚本的执行,我们把这种行为叫做全停顿,这种清除方式叫做全量清除(Stop-the-world sweeping)
它利用最新的和最好的垃圾回收技术来降低主线程挂起的时间, 比如:并行(parallel)清除,增量(incremental)清除和并发(concurrent)清除。
并行清除
并行垃圾回收是主线程和协助线程同时执行同样的工作,但是这仍然是一种全量清除的垃圾回收方式。很重要的特征就是虽然是多个线程通知回收,但是保证不会操作同一个对象。减少了停顿的时间。

增量清除
增量清除其实是在主线程上交替进行脚本执行和垃圾回收,和之前不同的地方其实就是,Oilpan 将大块的垃圾回收拆分成很多小的垃圾回收任务。
这种标记法并没有减少总的垃圾回收时间,甚至于会增加一点。但是这个避免了垃圾回收影响用户的操作等。

并发清除
随着应用程序及其生成的对象图越来越大,增量清除开始影响应用程序的性能。为了改善增量清除,我们开始利用并发清除来同时回收内存
并发清除是主线程和垃圾回收线程同时运行。主线程执行 JavaScript ,垃圾回收线程专注于垃圾回收。
Oilpan 强制终结器(finalizers)在主线程上运行,以帮助开发人员并排除应用程序代码内部的数据争用。为了解决此问题,Oilpan 将对象终结处理推迟到主线程。更具体地讲,每当并发清除程序遇到具有终结器(析构函数)的对象时,它将其推入终结队列(finalization queue),该队列将在单独的终结阶段中进行处理,该队列始终在运行应用程序的主线程上执行。并发清除的总体工作流程如下所示:

5. 内存泄漏
5. 内存泄漏和内存溢出的区别
内存泄漏:程序运行过程中,分配给内存的临时变量,用完之后却没有被回收。
内存溢出:简单的说就是程序运行过程中申请的内存大于系统能提供的内存,导致无法申请到足够内存。
他们之间的关系应该是:过多的内存泄漏最终会造成内存溢出。
5.2 常见的内存泄漏
特殊的闭包
只有应用了函数的内部变量的闭包才算是引发内存泄漏的闭包
function fn(){
let test = new Array(1000)
return function(){
console.log(test)
return test
}
}
let fnChild = fn()
fnChild()
test 变量的使用被外部调用了。所以他不能被回收。
优化:在用完闭包的时候,将其置为null
隐式全局变量
function fn(){
// 没有声明从而制造了隐式全局变量test1
test1 = new Array(1000).fill('isboyjc1') // 函数内部this指向window,制造了隐式全局变量test2
this.test2 = new Array(1000).fill('isboyjc2')
}
fn()
这里面使用test1就会被隐式的声明为全局变量。对于全局变量来说,垃圾回收很难判断什么时候不会被需要的。所以全局变量统称不会被回收。
优化:在不用变量的时候将其置空;使用let、const等去声明变量。
被遗忘的 DOM 引用
如果一个很大的DOM对象被引用而被忘记清除也会造成内存泄漏。
优化:在不用的时候将其置空。
被遗忘的定时器
像setTimeout 和 setInerval这种的定时器在不被清除的时候,是不会消失的。
优化:在不用的时候将其置空。
被遗忘的事件监听器
事件监听器和上面的定时器是一个原理,都需要手动去解除监听。
未被清理的 console 输出
浏览器保存了我们输出对象的信息数据引用,也正是因此未清理的 console 如果输出了对象也会造成内存泄漏
优化:及时清除代码中的console.log
Map和Set
当使用 Map 或 Set 存储对象时,同 Object 一致都是强引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收。
优化:使用WeakMap 以及 WeakSet

本文详细介绍了JavaScript的内存周期,包括内存分配、垃圾回收机制,如引用计数和标记-清除。重点讲解了V8引擎采用的分代式垃圾回收策略,新生代和老生代的处理方式,以及并行、增量和并发清除等优化技术。同时,讨论了内存泄漏和内存溢出的区别,并列举了常见的内存泄漏场景及优化方法。
1132

被折叠的 条评论
为什么被折叠?



