JS中垃圾数据是如何自动回收的

JS中垃圾数据是如何自动回收的

  • 背景
  • 垃圾回收机制
    • 调用栈中的数据回收
    • 堆空间中数据回收
      • 垃圾回收器的工作流程
        • 副垃圾回收器
        • 主垃圾回收器
      • 全停顿

背景

在JS栈和堆:数据是如何存储的一文中提到了 JavaScript 中的数据是如何存储的,并通过示例代码分析了原始数据类型时存储在栈空间中的,引用数据类型时存储在堆空间中的。通过这两种分配方式,解决了数据的内存分配问题。

不过有些数据在被使用了之后可能就不再需要了,这种称为垃圾数据。这些垃圾数据如果一直保存在内存中,内存会越用越多,所以需要对这些垃圾数据进行回收,以释放有限的内存空间。

垃圾回收机制

通常情况下,垃圾数据回收分为手动回收自动回收两种策略。

  • 手动回收:例外 C/C++ 就是使用手动回收策略,何时分配内存、何时销毁内存都是由代码控制的(调用 malloc 函数分配内存,然后再使用,当不再需要这块数据时,手动调用 free 函数来释放内存),如果一段数据已经不再需要了,但是又没有主动调用销毁函数来销毁,那么这种情况就被称为内存泄漏
  • 自动回收:像 JavaScript、Java、Python 等语言,产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放。

对于 JavaScript 而言,正是这个自动回收资源的特性带来了很多困惑,也让一些开发者误以为可以不关心内存管理,这是一个很大的误解。下面来探讨一下 JavaScript 中的栈中垃圾数据和堆中垃圾数据分别是如何被自动回收的。

调用栈中的数据回收

先看下面一段示例代码:

function foo() {var a = 1var b = {name: 'yy'}function showName() {var c = 2var d = {name: 'qq'} // 第 6 行}showName()
}
foo()

当执行到第 6 行代码时,其调用栈的堆空间状态图如下所示:

在这里插入图片描述

从图中可以看出,原始类型数据被分配到栈中,引用类型数据被分配到堆中。

在执行到 showName 函数时,JavaScript 引擎会创建 showName 函数的执行上下文,并将 showName 函数的执行上下文压入调用栈中,其调用栈状态图就如上图所示,同时还有一个记录当前执行状态的指针(ESP,指向调用栈中 showName 函数的执行上下文,表示当前正在执行 showName 函数。

接着,当 showName 函数执行完成后,函数执行流程就进入了 foo 函数,此时 JavaScript 会将 ESP 下移到 foo 函数的执行上下文,那这时就需要销毁 showName 函数的执行上下文了。

在这里插入图片描述

showName 函数执行结束后,ESP 向下移动到 foo 函数的执行上下文,此时 showName 的执行上下文虽然保存在栈内存中但是已经是无效内存了。此时如果 foo 函数再次调用另外一个函数,这块内容会被直接覆盖掉,用来存放另外一个函数的执行上下文。

堆空间中数据回收

所以,当 foo 函数执行结束后,ESP 应该是指向全局执行上下文,此时 showName 函数和 foo 函数的执行上下文就处于无效状态了,不过保存在堆中的两个对象依然占用着空间,如图所示:

在这里插入图片描述

也就是说,1003 和 1005 这两块内存依然被占用着,JavaScript 引擎是如何堆中的垃圾数据的呢?

垃圾回收器的工作流程

代际假说(The Generational Hypothesis)是垃圾回收领域中的一个重要术语,后续垃圾回收策略都是建立在该假说基础上的,而且不仅适用于 JavaScript,同样适用于大多数动态语言,如 Java、Python等。它有两个特点:

  • 大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问
  • 不死的对象,会活得更久

在 V8 中会把堆分为新生代老生代两个区域:新生代存放的是生存时间短的对象,老生代中存放的生存时间久的对象。新生区通常只支持 1-8M 容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器以便更高效地实施垃圾回收:

  • 副垃圾回收器,主要负责新生代的垃圾回收
  • 主垃圾回收器,主要负责老生代的垃圾回收
副垃圾回收器

通常情况下,大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收频率较高。

新生代中用 Scavenge 算法来处理,它把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如图所示

在这里插入图片描述

新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作:

首先要对对象区域中的垃圾做标记(标记空间中活动对象和非活动对象,所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象);标记完成后进入垃圾清理阶段(在所有标记完成后统一清理内存中所有被标记为可回收的对象),副垃圾回收器会把这些存活的对象复制到空闲区域中,同时还会把这些对象有序的排列起来,所以这个复制过程也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了(一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们将其称为内存碎片。当内存中出现了大量内存碎片后如果需要分配较大连续内存时就可能出现内存不足的情况,所以最后一步需要整理这些内存碎片,但这步是可选的)

完成复制后,对象区域与空闲区域进行角色翻转,这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。

由于新生代中的 Scavenge 算法在执行清理操作时,都需要将存活对象从对象区域复制到空闲区域,因此会产生复制的时间成本,如果新生区设置得太大了,每次清理的时间就会过长,所以为了执行效率,一般新生代的空间会被设置得比较小。

也正是因为新生代的空间不大,很容易被存活的对象装满,为解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

主垃圾回收器

主垃圾回收器主要负责老生中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。老生区对象有两个特点:占用空间大、存活时间长。

由于老生区对象比较大,若要在老生区中使用 Scavenge 算法进行垃圾回收是不合适的,因为复制这些大对象将会花费较多的时间,从而导致回收执行效率低下的同时还会浪费一半的空间,因此,主垃圾回收器采用的方式是标记-清除(Mark-Sweep)

首先是标记过程阶段。标记阶段是从一组根元素开始,递归遍历这组根元素,遍历过程中,能到达的元素称为活动对象,没有到达的元素判断为垃圾数据。

比如上面的示例代码中,当 showName 函数执行退出后,此时的调用栈和堆空间如图所示

在这里插入图片描述

showName 函数执行结束之后,ESP 向下移动,指向了 foo 函数的执行上下文,这时候如果遍历调用栈,是不会找到引用 1003 地址的变量的,也就意味着 1003 这块数据为垃圾数据,被标记为红色。由于 1005 这块数据被变量 b 引用了,所以这块数据会被标记为活动对象,这就是大致的标记过程。

接下来是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程完全不同,可以理解为这个过程是清除掉红色标记数据的过程,可参考下图大致理解

在这里插入图片描述

这种标记和清除的过程就是“标记-清除”算法,不过对一块内存多次执行标记-清除后,会产生大量不连续的内存碎片,而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记-整理(Mark-Compact)。这个标记过程仍然与标记-清除算法里一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,可参考下图大致理解

在这里插入图片描述

全停顿

由于 JavaScript 是运行在主线程上的,一旦执行垃圾回收算法,正在执行的 JavaScript 脚本都需要暂停下来,待垃圾回收完毕后再恢复脚本执行,我们将这种行为叫做全停顿(Stop-The-World)

在 V8 新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿的影响不大,但是老生代就不一样了。如果在执行垃圾回收过程中,占用主线程时间过长,例如 500ms,比如页面正在执行一个 JavaScript 动画,因为垃圾回收器在工作,就会导致这个动画在这 500ms 内无法执行,从而造成页面卡顿现象。

在这里插入图片描述

为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们将这个算法称为增量标记算法(Incremental Marking)。如图所示

在这里插入图片描述

使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React Native 桥接组件封装原生组件属性

自定义属性可以让组件具备更多的灵活性,所以有必要在JS 层通过自定义属性动态传值。 一、添加原生组件属性 因为 ViewManager 管理了整个组件的行为,所以要新增组件属性也需要在这里面(如 InfoViewManager)进行定义。 1、在Inf…

中通快递批量查询方法

你是否经常需要处理大量的中通快递单号,却苦于一个个等待查询?现在,有了固乔快递查询助手,这个问题迎刃而解!通过批量查询功能,你可以轻松管理、追踪你的中通快递单号,大大提高工作效率。 一、下…

H264码流进行RTP包封装

一.H264基本概念 H.264从框架结构上分为视频编码层(VCL)和网络抽象层(NAL),VCL功能是进行视频编解码,包括运动补偿预测,变换编码和熵编码等功能;NAL用于采用适当的格式对VCL视频数据…

Google ads谷歌广告投放详细步骤与技巧

对于跨境电商、独立站运营的卖家来说,谷歌广告投放是必备的流量拓展来源,但是在投入运营之前,你需要完整了解谷歌广告投放详细步骤,以为你丝滑地进行有效投放做好基础,下面为大家整理具体的谷歌投放技巧与步骤&#xf…

HCIP OSPF实验

任务: 1.使用三种解决ospf不规则区域的方法 2.路由器5、6、7、8、15使用mgre 3.使用各种优化 4.全网可达 5.保证更新安全 6.使用地址为172.16.0.0/16合理划分 7.每个路由器都有环回 拓扑图&IP划分如下: 第一步,配置IP&环回地址…

chat-plus部署指南

目录 1.下载代码 2.启动 3.测试 1.下载代码 cd /optwget https://github.com/yangjian102621/chatgpt-plus/archive/refs/tags/v3.2.4.1.tar.gz 2.启动 cd /opt/chatgpt-plus-3.2.4.1/deploydocker-compose up -d 3.测试 管理员地址xxx:8080/admin 账号密码admin/admin1…

vivado 使用项目摘要、配置项目设置、仿真设置

使用项目摘要 Vivado IDE包括一个交互式项目摘要,可根据设计动态更新命令被运行,并且随着设计在设计流程中的进展。项目摘要包括概览选项卡和用户可配置的仪表板,如下图所示。有关信息,请参阅《Vivado Design Suite用户指南&…

goland报错:The selected directory is not a valid home for Go SDK

原因: IDEA / goland无法识别到GO语言SDK版本 解决办法: 打开GO的安装目录下的src\runtime\internal\sys\zversion.go文件,添加一行(我的go版本是1.18.10) const TheVersion go1.18.10 重启goland再选择试试 最后…

【设计模式-04】Factory工厂模式

简要描述 简单工厂静态工厂工厂方法 FactoryMethod 产品维度扩展 抽象工厂 产品一族进行扩展Spring IOC 一、工厂的定义 任何可以产生对象的方法或类,都可以称之为工厂单例也是一种工厂不可咬文嚼字,死扣概念为什么有了new之后,还要有工厂&am…

科技创新领航 ,安川运动控制器为工业自动化赋能助力

迈入工业4.0时代,工业自动化的不断发展,让高精度运动控制成为制造业高质量发展的重要技术手段。北京北成新控伺服技术有限公司作为一家集工业自动化产品销售、系统设计、开发、服务于一体的高新技术企业,其引进推出的运动控制产品一直以卓越的…

详解Oracle数据库的启动

Oracle数据库的启动,其概念可参考Overview of Instance and Database Startup。 其过程可参见下图: 当数据库从关闭状态进入打开数据库状态时,它会经历以下阶段。 阶段Mount状态描述1实例在没有挂载数据库的情况下启动实例已启动&#xff…

共融共生:智慧城市与智慧乡村的协调发展之路

随着科技的飞速发展和全球化的不断深入,智慧城市和智慧乡村作为现代社会发展的重要组成部分,正逐渐成为人们关注的焦点。然而,在追求经济发展的过程中,城乡发展不平衡的问题也日益凸显。因此,如何实现智慧城市与智慧乡…

完整的模型验证套路

读取图片 from PIL import Imageimg_path "../Yennefer_of_Vengerberg.jpg" image Image.open(img_path) print(image)转换成灰度图(可选) image image.convert(L) image.show()转换成RGB格式 image image.convert(RGB)因为png格式是四…

STM32F103RCT6开发板M3单片机教程07-TIMER1CH1输出 PWM做LED呼吸灯

概述 本教程使用是(光明谷SUN_STM32mini开发板) 免费开发板 在谷动谷力社区注册用户,打卡,发帖求助都可以获取积分,当然最主要是发原创应用文档奖励更多积分. (可用积分换取,真的不用钱&…

Goby 漏洞发布|用友 NC registerServlet 反序列化远程代码执行漏洞

漏洞名称:用友 NC registerServlet 反序列化远程代码执行漏洞 English Name:Yonyou NC registerServlet Deserialize Remote Code Execute Vulnerability CVSS core: 9.8 影响资产数: 21320 漏洞描述: 用友 NC Cloud 是一种商…

熟悉HBase常用操作

1. 用Hadoop提供的HBase Shell命令完成以下任务 (1)列出HBase所有表的相关信息,如表名、创建时间等。 启动HBase: cd /usr/local/hbase bin/start-hbase.sh bin/hbase shell列出HBase所有表的信息: hbase(main):001:0> list(2)在终端输出指定表的所有记录数据。 …

k8s-存储 11

一、configmapu存储 首先,确保集群正常,节点都处于就绪状态 Configmap用于保存配置数据,以键值对形式存储。configMap资源提供了向 Pod 注入配置数据的方法,旨在让镜像和配置文件解耦,以便实现镜像的可移植性和可复用…

kylin集群反向代理(健康检查)

前面一篇文章提到了使用nginx来对kylin集群进行反向代理, kylin集群使用nginx反向代理-CSDN博客文章浏览阅读349次,点赞8次,收藏9次。由于是同一个集群的,元数据没有变化,所以,直接将原本的kylin使用scp的…

RabbitMQ(六)消息的持久化

目录 一、简介1.1 定义1.2 消息丢失的场景 二、交换机的持久化方式一:直接 new方式二:channel.exchangeDeclare()方式三:ExchangeBuilder【推荐】 三、队列的持久化方式一:直接 new方式二:channel.queueDeclare()方式三…

C++核心编程——类和对象(二)

本专栏记录C学习过程包括C基础以及数据结构和算法,其中第一部分计划时间一个月,主要跟着黑马视频教程,学习路线如下,不定时更新,欢迎关注。 当前章节处于: ---------第1阶段-C基础入门 ---------第2阶段实战…