前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >垃圾回收

垃圾回收

作者头像
Karl Du
发布2023-10-20 18:59:20
1590
发布2023-10-20 18:59:20
举报
文章被收录于专栏:Web开发之路Web开发之路

JavaScript 的垃圾回收是自动进行的,一般情况下,无需开发者去手动 GC。

你可能会好奇 JavaScript 是如何做到自动回收的?什么情况下变量不会被回收?我们编写代码的时候需要注意什么?

1、算法

垃圾回收有很多种算法,我们一一介绍

1.1 标记清除(Mark-and-Sweep)

标记清除算法:在程序运行时间中定期扫描内存中的对象,标记那些不再使用的对象,然后清除这些标记的对象。

弊端

  • 时间开销:因为在程序运行时间中需要定期扫描内存中的对象,标记那些不再使用的对象,然后清除这些标记的对象,所以会带来一定的时间开销。
  • 空间开销:在扫描内存对象的过程中,需要为每个对象额外分配一些空间来存储标记信息,这样会带来一定的空间开销。
  • 整理碎片:因为标记清除算法是通过标记某些对象来进行回收,所以会产生空间碎片,这些碎片可能会影响程序的性能。
  • 不能处理循环引用:标记清除算法只能处理那些不再使用的对象,如果存在循环引用的情况,可能会导致一些对象不能被正确回收。

1.2 引用计数(Reference Counting)

引用计数算法:每个对象都有一个引用计数器,当有变量或对象指向它时,该对象的计数器就会增加;当没有变量或对象指向它时,该对象的计数器就会减少。如果一个对象的计数器为 0,那么它就会被垃圾回收机制回收。

弊端

  • 复杂度:引用计数算法需要维护每个对象的引用计数器,每次对象引用关系发生变化时都需要更新计数器,这会带来较高的复杂度。
  • 无法处理循环引用:如果两个对象相互引用,但是都不再被使用,由于计数器都不为0,于是都不会被回收,这就会导致内存泄漏。
  • 无法处理闭包:当一个闭包中的变量不再使用时,对应的计数器不会变为0,这样就会导致闭包中的变量不能被回收。
  • 高开销:引用计数算法会对性能产生很大的开销,因为要不断的跟踪每个对象的引用关系。

1.3 可达性分析算法

可达性分析算法是一种基于标记清除的算法,它会在运行时间中扫描内存中的对象,标记那些可达的对象,然后清除那些不可达的对象。

注意:这种算法可以有效的防止内存泄漏,但会带来一定的性能开销。

1.4 分代回收算法

分代回收算法是一种垃圾回收算法,它将堆中的对象分成不同的代,每个代都有不同的回收策略。

新生代代表着新创建的对象,它们很可能很快就会被回收,所以新生代对象会被更频繁地扫描,并且当它们被标记为不可达时会被立刻回收。

而老生代代表着长期存在的对象,它们可能会持续存在很长时间,所以老生代对象会在运行时间长的情况下才会被回收。

分代回收算法的优点是可以更高效地回收内存,并且可以避免对短命对象进行过多的扫描和回收,提高性能。

通常分代回收算法都是基于标记-清除算法或标记-整理算法来实现的。

分代回收算法的一个缺点是需要额外的空间和时间来维护不同的代,并且在高负载下可能会导致更多的停顿。

总结来说, 分代回收算法是一种将堆中的对象分成不同的代,根据对象的存活时间来进行回收的算法,目的是提高回收的效率和性能。

2、回收时机

现代 JavaScript 的运行环境采用的是基于标记清除算法的垃圾回收机制,而且为了减少这种算法带来的性能开销,运行环境会在合适的时机进行垃圾回收,例如在程序执行过程中 空闲时间 进行垃圾回收,例如在程序执行时间过长或内存使用率过高时进行垃圾回收。

在运行环境中,垃圾回收算法会监测内存使用情况,当内存不足时会触发回收。

所以,当一个变量被标记清除时,它不是立刻被回收的,垃圾回收器会在运行时检查变量和对象的可达性,并在适当的时候回收不再使用的内存。这称为垃圾回收的延迟,因此程序员不需要关心垃圾回收的时间点。

3、GC 现状

3.1 不同浏览器的实现

每个浏览器都有自己的 JavaScript 引擎和垃圾回收机制

  • Google Chrome 浏览器使用 V8 引擎,它采用了增量标记清除算法和分代回收算法来进行垃圾回收
  • Mozilla Firefox 浏览器使用的是 SpiderMonkey 引擎,它采用了增量标记清除算法来进行垃圾回收
  • Microsoft Edge 浏览器使用 Chakra 引擎,它采用了标记-清除和引用计数算法结合的垃圾回收机制,并采用了分代回收的思想(2020年8月被微软抛弃,采用 Chromium 内核)
  • Safari 浏览器使用了 JavaScriptCore 引擎,它采用了标记-清除算法来进行垃圾回收

所以,每个浏览器都有自己的 GC 回收机制,它们在实现上可能略有不同,但都是为了解决同样的问题:自动回收不再使用的内存。

3.2 Chrome V8 两种算法交织

Chrome V8 使用增量标记清除算法来回收新生代对象,并使用分代回收算法来回收老生代对象。

新生代对象会被更频繁地扫描,并且当它们被标记为不可达时会被立刻回收,而老生代对象则在运行时间长的情况下才会被回收。这样做的目的是避免对短命对象进行过多的扫描和回收,提高性能。

总结来说, Google Chrome 浏览器使用的是 V8 引擎,它采用了增量标记清除算法和分代回收算法结合的垃圾回收机制。新生代对象采用增量标记清除算法回收,而老生代对象则采用分代回收算法回收。这样做的目的是为了提高回收效率和性能。

需要注意的是,V8引擎还采用了一些其他优化技术,比如说空间整理,这样可以减少空间碎片,更好地利用内存空间。

总之,V8的垃圾回收机制是一个非常复杂的系统,结合了多种算法和优化技术来实现高效的内存回收。

3、名词解释

空间碎片:

空间碎片是指在内存管理过程中由于垃圾回收机制的影响,造成的连续空间被分割成若干小的块,这些小的块称为空间碎片。这些空间碎片可能导致后续内存分配不能满足需求,降低程序性能。

空间碎片主要由基于标记清除算法的垃圾回收机制产生,因为这种算法会标记某些对象来进行回收,所以会产生空间碎片。空间碎片可能会影响程序的性能,因此需要进行整理。

举个例子: 假设当前程序使用了 100MB 的内存,其中有一块连续的 50MB 的空间被分配给了一个大对象。然后这个大对象被标记为不再使用,垃圾回收机制进行回收,释放了这 50MB 的空间。但是由于这块空间较大,可能不能被分配给小对象。于是这块空间就成为了一个空间碎片。 如果后续程序需要再次分配内存,但是这块空间碎片可能不能满足需求,那么程序就会继续分配新的内存,这样会导致内存碎片化,影响程序性能。

如果这个大空间碎片不去清理,那么就会导致这个程序占用内存越来越高

或许你回问,为什么大空间碎片不能给小对象?主要原因有以下几点:

  1. 内存对齐:空间碎片的大小可能不能满足系统的对齐要求,而小对象的内存分配需要满足对齐要求。
  2. 碎片管理:空间碎片可能不能被碎片管理算法进行整合和重新分配,因此不能分配给小对象。
  3. 空间分配策略:空间碎片可能并不能满足程序当前的空间需求,而小对象的内存分配需要满足程序的需求。

闭包:

闭包是指一个函数及其相关引用环境组成的包裹。在 JavaScript 中,当一个函数在另一个函数的作用域内被定义时,就会形成闭包。

闭包具有三个特征:

  1. 闭包可以访问它被定义时所在的作用域中的变量。
  2. 闭包可以访问它自己的参数和变量。
  3. 闭包可以被保存到变量中,并在稍后调用。

闭包的一个重要用途是封装私有数据和状态,它可以让你在不暴露实现细节的情况下提供封装的对象。它还可以用于编写模块化的代码。

在 JavaScript 中,闭包的作用域是保存在它被定义时的上下文中的,它可以访问到所有在该上下文中可以访问到的变量。这意味着,闭包可以访问它所在函数的作用域中的变量,以及它所在的全局作用域中的变量。

例如:

代码语言:javascript
复制
function outerFunction(x){
  var innerVar = 1;
  function innerFunction(y){
    return x + y + innerVar;
  }
  return innerFunction;
}

var closure = outerFunction(5);
console.log(closure(10)); // 16

在上面这个例子中, innerFunction 为闭包,它被定义在 outerFunction 内部。闭包可以访问 outerFunction 中的变量 x 和 innerVar,并且在调用 closure(10) 时可以返回正确的结果 16。

在上面这个例子中, innerFunction 为闭包,它被定义在 outerFunction 内部。闭包可以访问 outerFunction 中的变量 x 和 innerVar,并且在调用 closure(10) 时可以返回正确的结果 16。

闭包可以保存上下文状态,它能记住它被定义时的环境,并在以后使用。由于闭包引用了它外部作用域中的变量,因此闭包可能会导致内存泄露,如果不小心使用。因为闭包会持有它所引用的变量,这些变量不能被垃圾回收器回收。

例如:

代码语言:javascript
复制
function setupEventListeners() {
  var elements = document.querySelectorAll('.clickable');
  for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener('click', function() {
      console.log(i);
    });
  }
}

上面的例子中, addEventListener 函数中的匿名函数是闭包,它引用了外部作用域中的 i 变量,当点击元素时,会持有 i 变量的值,如果 setupEventListeners 函数已经被调用并执行完成,那么 i 会变成最后的值,而不是当时的值,这就是一个闭包带来的问题。

总结来说,闭包是 JavaScript 中一个重要的概念,它允许函数访问它被定义时所在的作用域中的变量。闭包可以用来封装私有数据和状态,实现模块化编程。但是也要注意,如果不小心使用闭包可能会导致内存泄露和其他问题。

4、总结

说了这么多,我们明白现代 JavaScript 引擎使用的是标记清除算法去回收垃圾,一般情况下,我们不需要去关心垃圾回收什么时候去进行的。但是我们要注意标记清除有几个弊端,导致有些情况下,垃圾无法被回收。

1、全局变量下挂载的变量无法被回收

2、一个对象被闭包引用,或者被事件监听,它也无法被回收

3、垃圾回收器无法回收循环引用,需要手动解除引用关系释放内存

本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023/01/17 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、算法
    • 1.1 标记清除(Mark-and-Sweep)
      • 1.2 引用计数(Reference Counting)
        • 1.3 可达性分析算法
          • 1.4 分代回收算法
          • 2、回收时机
          • 3、GC 现状
            • 3.1 不同浏览器的实现
              • 3.2 Chrome V8 两种算法交织
              • 3、名词解释
              • 4、总结
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
              http://www.vxiaotou.com