你真的了解JS垃圾回收机制吗?

news2024/11/14 14:00:11

目录

前言

堆栈内存管理

JS垃圾回收机制

标记清除(Mark and Sweep)

标记阶段

清除阶段

标记清除的特点

优点

缺点

引用计数(Reference Counting)

引用计数器的维护

引用计数的跟踪

垃圾回收的触发

回收对象

引用计数的特点

优点

缺点

分代回收(Generational Collection)

老生代回收

新生代回收

分代回收的特点

优点

缺点

内存泄漏

内存泄漏的场景

无用的对象引用

循环引用

全局变量的滥用

未释放的资源

总结

相关代码


前言

垃圾回收是JavaScript中内存管理的重要组成部分。开发人员不需要手动分配和释放内存。垃圾回收机制可以自动处理内存的分配和释放,减轻了开发人员的负担,并且降低了内存泄漏的风险,它的主要目的是自动地检测和释放不再使用的内存,以便程序能够更高效地利用系统资源。

它通过标记不再需要的对象,并回收它们所占用的内存空间,以便其他对象可以使用。

本篇文章将与大家分享,介绍一下JavaScript垃圾回收的重要性和定义,并深入探讨内存管理的概念、JS垃圾回收机制的分类,以及如何避免内存泄漏以及性能优化。

堆栈内存管理

在之前的文章中,我针对堆与栈的概念做了初步的介绍,引用文章中的一句话:

栈内存用于存储程序的函数调用,变量声明以及一些占用小的变量值,如布尔,部分整数等,它们的生命周期受到函数的调用和退出以及变量的作用域的控制。当函数被调用或者变量创建时,相关的变量和函数调用会被压入栈内存,如果函数退出或者变量作用域销毁,相关的变量和函数就会从栈内存中弹出。

堆内存的作用是存储变量值,如字符串,对象,数组及函数,它们的生命周期受到JavaScript垃圾回收机制的控制,当不再需要这些变量时,垃圾回收机制会将它们销毁。

简言之,堆用于存储动态分配的对象,而栈用于存储基本类型的值和对堆中对象的引用。

也就是说,在堆内存中才存在垃圾回收器这个概念,内存的分配和释放是由JavaScript引擎自动处理的,开发人员无需显式地分配或释放内存。JavaScript引擎使用垃圾回收机制来管理内存,确保不再使用的对象被自动回收,以便为新的对象腾出空间。

JS垃圾回收机制

进入今天的正题,垃圾回收机制有三类,其中标记清除和引用计数是比较常见的机制,分代回收则是前二者的结合

标记清除(Mark and Sweep)

标记清除法是JS最常见的垃圾回收机制之一。它的工作流程包括标记阶段和清除阶段。

标记阶段

  1. 从根对象开始,例如全局对象(window)或函数的作用域链
  2. 遍历对象的属性和引用,将可访问的对象标记为被引用的对象
  3. 递归遍历活动对象的属性和引用,标记其他可访问的对象

清除阶段

  1. 遍历堆中的所有对象。
  2. 对于未被标记为活动的对象,将其标记为垃圾对象。
  3. 释放垃圾对象所占用的内存空间。
  4. 将已经被清除的对象从内存中删除。

我们写个类来模拟一下标记清除的操作

// 标记清除, 垃圾回收机制
class MarkGC {
  marked = new Set(); // 模拟标记操作
  run(obj) {
    this.marked.clear(); // 这一步应该是放在最后的,但是看不出效果,所以改成运行前重置
    this.mark(obj);
    this.sweep(obj); // 这一步实际上没有效果,为了方便理解
    return this;
  }
  //   判断对象或属性是否已经标记
  checkMark = (obj) => typeof obj === "object" && !this.marked.has(obj);
  mark(obj) {
    const { marked } = this;
    if (this.checkMark(obj)) {
      marked.add(obj);
      Reflect.ownKeys(obj).forEach((key) => this.mark(obj[key]));
    }
  }
  sweep(obj) {
    Reflect.ownKeys(obj).forEach((key) => {
      const it = obj[key];
      if (this.checkMark(it)) {
        delete obj[key];
        this.sweep(it);
      }
    });
  }
}
// 全局对象
const globalVar = {
    obj1: { name: "Object 1" },
    obj2: { name: "Object 2" },
    obj3: { name: "Object 3" }
}
const gc = new MarkGC()
gc.run(globalVar)// 执行垃圾回收
console.log(globalVar, gc.marked);
// 删除操作
delete globalVar.obj3
delete globalVar.obj2
// 对象删除后运行垃圾回收
gc.run(globalVar)
console.log(globalVar, gc.marked);

来理解一下上述代码,标记清除法主要分为mark操作和sweep操作,运行mark函数会将全局对象中的属性存入标记列表中,然后运行sweep函数对,没标记的对象清除

标记清除的特点

优点

  • 内存回收全面:标记清除算法能够回收不再被引用的所有对象,包括循环引用的对象。通过标记阶段和清除阶段的组合,能够有效地释放内存空间
  • 灵活性:标记清除算法与编程语言的具体实现无关,适用于多种编程语言和环境。它可以在运行时动态地进行垃圾回收,根据对象的实际引用情况进行操作
  • 可预测性:标记清除算法的执行时间是可控的。垃圾回收操作可以在合适的时机进行,避免了出现大量的内存分配和释放操作,从而提高了程序的响应性能

缺点

  • 暂停时间:标记清除算法需要在垃圾回收时停止程序的执行,进行标记和清除操作。这可能导致程序的暂停时间较长,影响了程序的实时性和响应性能
  • 空间效率:标记清除算法在执行清除操作时,需要对整个堆进行遍历,查找并清除未标记的对象。这可能导致在垃圾回收期间出现较大的内存占用,从而降低了内存的利用效率
  • 碎片化问题:标记清除算法在清除对象后会产生内存碎片,即一些小而不连续的内存空间。这可能会导致后续的内存分配操作出现困难,增加内存分配的时间和复杂度

引用计数(Reference Counting)

引用计数基于每个对象维护一个引用计数器,用于跟踪对象被引用的次数。当对象的引用计数变为零时,即没有任何引用指向它时,该对象被认为是不再被使用,可以进行回收。下面是该方法的基本原理

引用计数器的维护

  1. 每个对象都有一个引用计数器,初始值为 0。
  2. 当对象被引用时,引用计数器增加。
  3. 当对象的引用被取消或销毁时,引用计数器减少。

引用计数的跟踪

  1. 当一个对象被其他对象引用时,引用计数增加。
  2. 当一个对象引用的其他对象被取消或销毁时,引用计数减少。

垃圾回收的触发

  1. 在程序执行过程中,当垃圾回收器被触发时,它会遍历堆中的所有对象。
  2. 对于每个对象,检查其引用计数器的值。
  3. 如果引用计数器为零,说明该对象不再被引用,可以被回收。

回收对象

  1. 当一个对象被回收时,其占用的内存空间会被释放。
  2. 同时,该对象引用的其他对象的引用计数也会相应减少。
  3. 如果其他对象的引用计数也变为零,这些对象也会被回收,整个过程递归进行。

我们同样使用一段代码来简单模拟一下引用计数的操作

// 引用计数器
class RefCount {
  constructor() {
    this.count = 0;
  }

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }
}

// 对象类
class MyObject {
  constructor() {
    this.refCount = new RefCount();
    this.refCount.increment(); // 对象被创建时,引用计数加1
  }

  addReference() {
    this.refCount.increment(); // 引用增加时,引用计数加1
  }

  releaseReference() {
    this.refCount.decrement(); // 引用减少时,引用计数减1
    if (this.refCount.count === 0) {
      this.cleanup(); // 引用计数为0时,进行清理操作
    }
  }

  cleanup() {
    // 执行清理操作,释放资源
    console.log("清理完成");
  }
}
// 创建对象并建立引用关系
const obj1 = new MyObject();
// 建立引用关系
obj1.addReference();
console.log(obj1.refCount);
// 解除引用关系
obj1.releaseReference();
obj1.releaseReference();
console.log(obj1.refCount);

RefCount类是一个简单的计数器,使用MyObject类创建新的类,使用计数器的addReference函数增加引用数量,使用releaseReference解除引用关系,此时数量会减一,当引用数量减到0时会执行cleanup函数对资源进行释放,达到垃圾回收效果

引用计数的特点

优点

  • 实时性:引用计数算法能够实时地检测到对象的不再被引用状态,并立即回收这些对象。一旦对象的引用计数变为零,即可立即进行回收,释放对象所占用的内存空间

  • 简单高效:引用计数算法的实现相对简单,每个对象都维护一个引用计数器,通过增加和减少计数器的值来追踪对象的引用关系,这使得引用计数算法在实现上比较高效
  • 处理循环引用:引用计数算法通常能够处理循环引用的情况,即当两个或多个对象互相引用时,只要它们的引用计数都变为零,垃圾回收器就能够回收这些对象

缺点

  • 循环引用问题:引用计数算法无法处理循环引用的情况。当存在循环引用时,即使这些对象不再被程序使用,它们的引用计数也不会变为零,从而导致内存泄漏
  • 额外开销:引用计数算法需要维护每个对象的引用计数器,这会带来额外的内存开销。每次对象的引用发生变化时,都需要更新计数器的值,这会增加运行时的开销
  • 更新的性能开销:当对象的引用发生频繁变化时,如大量的增加和减少引用,引用计数的频繁更新可能会影响程序的性能

分代回收(Generational Collection)

分代回收是一种结合了标记清除和引用计数的垃圾回收机制,它会根据对象的生命周期将内存分为不同的代。

分代回收存在一个假设:大多数对象的生命周期都比较短暂,而只有少数对象具有较长的生命周期。基于这个假设,分代回收将对象的生命周期划分为两类:新生代(Young Generation)堆和老生代(Old Generation)堆。新生代堆用于存储大量的短期存活对象,而老生代堆则用于存储长期存活对象

关于两种分代回收的原理如下

老生代回收

老生代实际上就是上面说到的标记清除算法,这套算法适用于存活时间较长的对象

新生代回收

新生代堆被分为两个相等大小的区域:From空间和To空间

  1. 新对象分配到From空间
  2. 当From空间满时,触发垃圾回收
  3. 从根对象开始,标记所有存活的对象
  4. 将存活的对象复制到To空间中
  5. 清除已经死亡的对象
  6. 将To空间作为新的From空间,并将From空间作为新的To空间,完成垃圾回收

下面我使用JS实现一下新生代回收的过程

// 新生代回收机制
class GenerationalCollection {
  // 定义堆的From空间和To空间
  fromSpace = new Set();
  toSpace = new Set();
  garbageCollect(obj) {
    this.mark(obj); // 标记阶段
    this.sweep(); // 清除阶段
    // 切换From和To的空间
    const { to, from } = this.exchangeSet(this.fromSpace, this.toSpace);
    this.fromSpace = from;
    this.toSpace = to;
    return this;
  }
  isObj = (obj) => typeof obj === "object";
  exchangeSet(from, to) {
    from.forEach((it) => {
      to.add(it);
      from.delete(it);
    });
    return { from, to };
  }
  allocate(obj) {
    this.fromSpace.add(obj);
  }
  mark(obj) {
    if (!this.isObj(obj) || obj?.marked) return;
    obj.marked = true;
    this.isObj(obj) &&
      Reflect.ownKeys(obj).forEach((key) => this.mark(obj[key]));
  }
  sweep() {
    const { fromSpace, toSpace } = this;
    fromSpace.forEach((it) => {
      if (it.marked) {
        // 将标记对象放到To空间
        toSpace.add(it);
      }
      // 从From空间中移除该对象
      fromSpace.delete(it);
    });
  }
}
// 全局对象
const globalVar = {
    obj1: { name: "Object 1" },
    obj2: { name: "Object 2" },
    obj3: { name: "Object 3" }
}
const GC = new GenerationalCollection()
// 创建对象并分配到From空间
GC.allocate(globalVar.obj1)
GC.allocate(globalVar.obj2)
console.log(GC.fromSpace, GC.toSpace);
// 执行垃圾回收
GC.garbageCollect(globalVar)
console.log(GC.fromSpace, GC.toSpace);

简单描述一下上面的代码,allocate函数将对象放到From堆空间中,mark函数对对象及属性添加标记,在sweep清除函数中如果对象既被标记又在From空间中那么就将其复制到To空间中,最后在垃圾回收机制函数garbageCollect中对调两个堆空间最终完成整个周期

分代回收的特点

优点

  • 提高回收效率:分代回收能够针对对象的生命周期进行不同的优化。通过区分对象所在的代,可以针对不同代采用更适合的回收策略。由于新生代对象的生命周期较短,采用复制算法进行回收可以快速地清理掉大部分垃圾对象。而老生代对象的生命周期较长,使用标记清除法进行回收可以更全面地清理垃圾对象。
  • 减少停顿时间:分代回收可以将垃圾回收任务分散到不同的时间段进行,避免一次性处理所有对象。这样可以减少单次垃圾回收的时间,从而减少系统的停顿时间,提高系统的响应能力和用户体验。

缺点

  • 需要维护多个代:分代回收需要维护不同代的对象,增加了内存管理的复杂性。
  • 内存分配和复制开销:新生代回收中使用的复制算法需要将存活的对象复制到新的空间中,这会引入一定的内存分配和复制开销。同时,分代回收中的对象移动和内存重整等操作也会带来一定的开销

内存泄漏

内存泄漏是指在程序中分配的内存无法被正常释放和回收的情况,导致内存的持续占用和增长。

它与垃圾回收机制有密切关系。垃圾回收机制的目的是自动识别和回收不再使用的内存,以避免内存泄漏和资源浪费。然而,如果存在内存泄漏,即使对象已经不再使用,垃圾回收机制也无法正确识别这些对象为垃圾并释放它们的内存。这样,内存泄漏导致的内存占用会随着时间的推移逐渐增加,直到达到系统的内存限制。

内存泄漏的场景

常见的内存泄漏场景有下面几类

无用的对象引用

当对象仍然存在引用,即使不再需要时,垃圾回收机制也无法回收这些对象。例如,未正确解除事件监听器或定时器,导致被监听的对象一直被引用,无法释放内存。

场景:使用element.addEventListener却没有使用取消函数:removeEventListener;setInterval或setTimeout没有关闭

解决:使用removeEventListener,clearTimeout等函数重置

循环引用

当两个或多个对象相互引用,并且这些对象之间没有与其他对象的引用关系时,即使这些对象不再被使用,垃圾回收机制也无法回收它们。这种情况下,对象之间形成了一个封闭的循环,导致内存泄漏。

场景:

const obj = {}
const obj1 = {}
obj.child = obj1
obj1.child = obj

解决:合理设计对象之间的引用关系,避免对象类型变量循环使用,使用弱引用或断开循环引用的方法来解决

全局变量的滥用

全局变量在整个应用程序生命周期中都存在,如果没有正确管理和释放全局变量,会导致这些变量一直存在于内存中,无法被垃圾回收机制回收。

场景:全局创建变量,在程序或页面的生命周期并未对该变量重置或者清空,则会一直处于激活状态,不会被垃圾回收机制处理

解决:限制变量的作用域,避免过多的全局变量,TS中可以使用命名空间和模块的形式,也就是JS的函数或对象

未释放的资源

例如打开的文件句柄、网络连接或数据库连接等资源,如果在使用完毕后没有正确释放,会导致内存泄漏。

场景:在网络请求时超时时间过长,请求一直等待可能会造成内存泄漏

解决:使用完操作后尽量手动断开或者设置超时,比如请求的abort函数和timeout属性,这一类现象类似于线程的死锁,无法得知何时取消,造成性能问题。

总结

JavaScript垃圾回收机制是内存管理的关键,它能够自动检测和释放不再使用的内存,提高程序的性能和可靠性。了解垃圾回收的分类、内存泄漏的原因和避免方法,以及性能优化的最佳实践,有助于开发高效的JavaScript应用程序。
以上就是文章的全部内容了,感谢你看到了这里,希望你从中获益,如果觉得文章不错的话,还希望三连支持一下博主,非常感谢!

相关代码

myCode: 基于js的一些小案例或者项目 - Gitee.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/736469.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

视频转音频MP3格式怎么做?教你几种转换小妙招

当我们需要编辑视频中的声音,例如去除噪音、调整音量、加入配乐等,此时需要先将视频中的音频提取出来进行编辑,再将编辑后的音频重新与视频合并,以便达到一个最佳效果。那么怎么将视频转换成MP3格式的音频文件呢?教大家…

SpringBoot项目多模块打包部署Docker实战

前言 我们好多程序员都只关注功能代码的编写,在一些运维工作上则显得略有不足。这篇文章通过介绍最常见的Maven管理的Spring Boot项目多模块打包部署Docker来介绍一下项目部署过程中操作流程和几个需要注意的点。文章假设读者有前面提到的技术点的前置知识&#xf…

C#(五十八)之C#List

前几天&#xff0c;看同事写的代码中有list相关的字眼&#xff0c;百度了一下&#xff0c;原来是C#中list泛型集合。 了解一下。 List&#xff1a;泛型集合&#xff0c;List<T>类是 ArrayList 类的泛型等效类。该类使用大小可按需动态增加的数组实现 IList<T> 泛型…

Maven工程分模块开发讲解及入门案例

1.分模块开发的意义 一个模块只做自己对应的功能&#xff0c;提升开发效率&#xff0c;将一个工程拆分成若干个子模块方便之间相互调用&#xff0c;接口共享&#xff0c;降低耦合度提高代码复用率。 2.分模块开发入门案例 下面将domain这个模块从当前模块当中给拆分出来。 …

开心档之CSS 测验

目录 CSS 测验 CSS 测验 CSS测验是一种衡量前端开发人员对CSS的熟练程度的测试。通过CSS测验&#xff0c;可以评估一个人对CSS语言的掌握程度和应用能力&#xff0c;帮助公司或招聘方挑选合适的人才。下面将介绍如何进行CSS测验以及一些常见的CSS考题。 一、CSS测验的类型 1…

OpenCV 入门教程:寻找和绘制轮廓

OpenCV 入门教程&#xff1a;寻找和绘制轮廓 导语一、寻找轮廓二、绘制轮廓三、示例应用3.1 目标检测和定位3.2 图像分割 总结 导语 寻找和绘制轮廓是图像处理中常用的技术之一&#xff0c;用于识别、定位和分析图像中的目标区域。在 OpenCV 中&#xff0c;寻找和绘制轮廓可以…

「2024」预备研究生mem-行程问题

一、行程问题 二、课后题 往返 上山下山

LeetCode[75]颜色分类

难度:Medium 题目&#xff1a; 给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums &#xff0c;原地对它们进行排序&#xff0c;使得相同颜色的元素相邻&#xff0c;并按照红色、白色、蓝色顺序排列。 我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 必须在不使用库…

【前端面试专栏】用户输入网址到页面返回都发生了什么?

&#x1f431; 个人主页&#xff1a;不叫猫先生&#xff0c;公众号&#xff1a;前端舵手 &#x1f64b;‍♂️ 作者简介&#xff1a;2022年度博客之星前端领域TOP 2&#xff0c;前端领域优质作者、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步…

软件安全测试流程与方法分享(下)

安全测试是在IT软件产品的生命周期中&#xff0c;特别是产品开发基本完成到发布阶段&#xff0c;对产品进行检验以验证产品符合安全需求定义和产品质量标准的过程。安全是软件产品的一个重要特性&#xff0c;也是CNAS测试认证中非常重要的项目&#xff0c;本系列文章我们与大家…

linux 信号原理 信号处理设置signal, 信号发送kill,信号等待sigsuspend,信号阻塞sigprocmask,一网打尽信号使用

​专栏内容&#xff1a; postgresql内核源码分析 手写数据库toadb 并发编程 个人主页&#xff1a;我的主页 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 概述 信号是一种软中断的方式&#xff0c;让进程陷入中断处理调…

vector的resver和resize

#include <iostream> #include <vector> using namespace std; int main() {std::vector<std::vector<std::vector<int> > > a(2);//创建2个vector<vector<int> >类型的数组vector<int> vec;//vec.resize(10); //结果1vec.res…

【Linux之拿捏信号3】阻塞信号

文章目录 相关概念原理sigset_t信号集信号集操作函数sigprocmask系统调用sigpending 相关概念 实际执行信号的处理动作——信号递达Delivery&#xff08;例如自定义捕捉动作&#xff0c;core&#xff0c;Term终止进程的动作&#xff09;。信号从产生到递达之间的状态——信号未…

Verilog基础之十四、FIFO实现

目录 一、FIFO 1.1 定义 1.2 实现方式 1.3 实现原理 二、代码实现 三、仿真结果 3.1 复位阶段 3.2 写入阶段 3.3 读取阶段 3.4 同时读写或不读不写 四、参考资料 一、FIFO 1.1 定义 FIFO(First in First out)为先进先出队列&#xff0c;具有存储功能&#xff0c;…

一篇带你彻底搞懂线程池

目录 一、自定义线程池 1、产生背景 2、堵塞队列 3、线程池 4、拒绝策略 二、ThreadPoolExecuor 1、线程池状态 2、构造方法 3、newFixedThreadPool 4、newCachedThreadPool 5、newSingleThreadExecutor 6、提交任务 7、关闭线程池 三、异步模式之工作线程 1、定…

C-数据的储存(上)

文章目录 前言&#x1f31f;一、数据类型详细介绍&#x1f30f;1.内置类型&#x1f4ab;&#xff08;1&#xff09;.整形家族&#x1f4ab;&#xff08;2&#xff09;.浮点数家族&#x1f30f;2.构造类型&#xff08;也称自定义类型&#xff09;&#x1f30f;3.指针类型&#x…

OpenCV 入门教程:Haar特征分类器

OpenCV 入门教程&#xff1a; Haar 特征分类器 导语一、Haar特征分类器原理二、Haar特征分类器步骤三、示例应用总结 导语 Haar 特征分类器是图像处理中常用的目标检测算法&#xff0c;用于识别图像中的特定目标。该算法基于 Haar-like 特征模板&#xff0c;通过训练分类器来实…

ArcGIS PRO基础教程(一)

操作要求 1.面积为50-80亩 2.不能选在有耕地、园地内 3.坡度小于15度,高程在以下1930 4.距离水源地在300米以内 已知数据 1.等高线图 CONTOUR 2.土地利用图 parcel 3.水系图 water 操作步骤 创建工程,模板选地图就可以了(注:在arcgis pro中创建工程可以看作在arcg…

大火的ChatGPT与表格插件结合会有哪些意想不到的效果?

大火的ChatGPT与表格插件结合会有哪些意想不到的效果&#xff1f; 摘要&#xff1a;本文由葡萄城技术团队于CSDN原创并首发。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 ChatGPT已经火了好…

前端全集Ⅰ---- HTML/CSS/JavaScript

一 介绍web开发 Web&#xff1a;全球广域网&#xff0c;也称万维网&#xff0c;能够通过浏览器访问的网站 Web网站的工作流程&#xff1a;&#xff08;前后端分离模式&#xff09; 网页有哪些组成&#xff1f; 文字、图片、视频、音频、超链接 前端代码通过浏览器的解析和渲…