JavaScrip中的内存管理

JavaScript 内嵌了垃圾回收器

1. 内存周期

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放、归还

2. 内存分配

JavaScript 中,内存一般分为栈内存堆内存基本类型储存在栈内存栈内存由操作系统管理;引用类型储存在堆内存堆内存由引擎管理。

3. 内存的回收和释放

常说的内存管理是在堆内存上的。内存的回收,也就是垃圾回收机制是有算法(策略)的。最常见的有两种,一种是引用计数,还有一个是标记-清除

3.1 引用计数

最早期的垃圾回收策略是引用计数

思路:对每个值都记录它的引用次数。声明变量并引用时,这个值得引用数就会加1,类似地,如果对值引用的变量被其他值覆盖了,那么引用数就减1。垃圾回收程序下次运行就会释放引用数为0的内存。

引用计数有一个严重的问题:循环引用,就是对象A有一个指针指向对象B,而对象B也引用了对象A。它们永远不会被回收。这就会造成内存泄漏

早期的 IE 浏览器使用的就是引用计数

优点:

  1. 立即回收。当一个内存的引用为0的时候,这部分的内存会被立即回收。
  2. 减少程序的暂停。当前执行平台的内存肯定是有上限的,所以内存肯定有占满的时候。由于引用计数算法是时刻监控着内存引用值为0的对象,保证了当前内存是不会有占满的时候。

缺点:

  1. 循环引用对象无法被回收。按照引用计数的回收标准,函数内部的循环引用的对象是没法被回收的,因为他们的引用数永远不会是0。
  2. 计数比较耗时。需要频繁的去查看哪些对象引用数为0了,当维护的对象数大的时候,查找的过程就比较耗时了。引用计数的GC会占用主线程,会阻塞其他任务的运行。

3.2 标记-清除

这个策略分为标记清除两个阶段。标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。

可达性:就是可以到达,因为堆内存中往往都存在根对象(不是传统意义上的对象)。我们标记也是从根对象上去开始递归遍历的。当一个访问一个对象的路径内切断了,他就是不可达的。那么就需要被清理

所以,当函数执行完毕的时候,当前函数就和全局断开了连接。这就是我们一般是没法访问函数内部定义的变量的原因(特殊的闭包除外)。这时候这些变量就会因为没法到达而无法标记,所以就会在下次的 GC 的时候被回收。

标记清除引用计数的最大区别就是,回收的标准的不同。零引用的内存一定不可到达,但是非零引用的内存不一定可达到。

标记清除算法大致流程:

  • 垃圾收集器给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
  • 然后从各个根对象开始遍历,把不是垃圾的节点改成1
  • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
  • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收

优点:

  1. 实现简单。打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记。

缺点:

  1. 内存碎片化。在回收内存后,原来对象占用的位置被空下来,这就造成了内存的不完整性
    在这里插入图片描述
  2. 分配速度慢。将这些碎片化的内存重新分配是需要时间的,一般采用以下方案:
    • First-fit,就是找到大于或者等于需要分配内存大小的内存后,就立刻返回。(常用)
    • Best-fit,查找整个内存,直到找到符合待分配大小的最小的内存块。
    • Worst-fit,查找整个内存,找到最大的内存块,先切除需要分配大小的内存部分,然后返回剩下的。
      在这里插入图片描述

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

4. V8 引擎的垃圾回收机制

V8 是一个由 Google 开源的高性能 JavaScript引擎,其源代码使用 C++ 编写。V8 被用于 Google 的开源浏览器 Chrome 中,同时也被用于 Node.js,以及其他一些软件中。

V8 的垃圾回收也是基于标记清除算法

4.1 分代式垃圾回收

将堆内存分为新生代老生代两个区域,然后两个区域采用不同的垃圾回收策略。

新生代:生命周期短,占用内存小

老生代:生命周期长或者占用大小比较大。
在这里插入图片描述

新生代

新生代的对象一般是存活时间比较短比较小的对象。这部分通常只有很小的内存分配,一般是 1~8M 的容量。

Cheney算法将新生代中的内存一分为二,一个是出于使用状态的区域,我们称之为 使用区,一个是出于闲置状态的 称之为空闲区

流程:首先先进入标记流程,新生代垃圾回收器找出使用区内的活动的对象进行标记,接着就是将被标记的对象复制空闲区并排序。随后,进入垃圾清洁阶段,将使用区中的未被标记的空间清理掉,最后将两个区域角色互换一下。

在这里插入图片描述

晋升规则:

  1. 在一个变量的移动阶段,如果这个变量所占用的空间的大小超过空闲区的25%,那么这个变量会直接晋升为老生代
  2. 当一个变量已经进过了多次交换后存活,那么这个变量也会晋升为老生代

老生代

主要使用了标记-清除算法。在此基础上,使用了许多优化技术。

全量清除: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就会被隐式的声明为全局变量。对于全局变量来说,垃圾回收很难判断什么时候不会被需要的。所以全局变量统称不会被回收

优化:在不用变量的时候将其置空;使用letconst等去声明变量。

被遗忘的 DOM 引用

如果一个很大的DOM对象被引用而被忘记清除也会造成内存泄漏。

优化:在不用的时候将其置空。

被遗忘的定时器

setTimeoutsetInerval这种的定时器在不被清除的时候,是不会消失的。

优化:在不用的时候将其置空。

被遗忘的事件监听器

事件监听器和上面的定时器是一个原理,都需要手动去解除监听。

未被清理的 console 输出

浏览器保存了我们输出对象的信息数据引用,也正是因此未清理的 console 如果输出了对象也会造成内存泄漏

优化:及时清除代码中的console.log

Map和Set

当使用 MapSet 存储对象时,同 Object 一致都是强引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收。

优化:使用WeakMap 以及 WeakSet

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值