我的编程空间,编程开发者的网络收藏夹
学习永远不晚

聊聊JavaScript内存管理

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

聊聊JavaScript内存管理

本文已经过原作者Ahmad shaded 授权翻译。

大多数时候,我们在不了解有关内存管理的知识下也只开发,因为 JS 引擎会为我们处理这个问题。不过,有时候我们会遇到内存泄漏之类的问题,这个只有知道内存分配是怎样工作的,我们才能解决这些问题。

在本文中,主要介绍内存分配和垃圾回收的工作原理以及如何避免一些常见的内存泄漏问题。

缓存( Memory)生命周期

在 JS 中,当我们创建变量、函数或任何对象时,J S引擎会为此分配内存,并在不再需要时释放它。

分配内存是在内存中保留空间的过程,而释放内存则释放空间,准备用于其他目的。

每次我们分配一个变量或创建一个函数时,该变量的存储会经历以下相同的阶段:

分配内存

JS 会为我们处理这个问题:它分配我们创建对象所需的内存。

使用内存

使用内存是我们在代码中显式地做的事情:对内存的读写其实就是对变量的读写。

释放内存

此步骤也由 JS 引擎处理,释放分配的内存后,就可以将其用于新用途。

内存管理上下文中的“对象”不仅包括JS对象,还包括函数和函数作用域。

内存堆和堆栈

现在我们知道,对于我们在 JS 中定义的所有内容,引擎都会分配内存并在不再需要内存时将其释放。

我想到的下一个问题是:这些东西将被储存在哪里?

JS 引擎在两个地方可以存储数据:内存堆和堆栈。堆和堆栈是引擎是用于不同目的的两个数据结构。

堆栈:静态内存分配

堆栈是 JS 用于存储静态数据的数据结构。静态数据是引擎在编译时能知道大小的数据。在 JS 中,包括指向对象和函数的原始值(strings,number,boolean,undefined和null)和引用类型。

由于引擎知道大小不会改变,因此它将为每个值分配固定数量的内存。

在执行之前立即分配内存的过程称为静态内存分配。这些值和整个堆栈的限制取决于浏览器。

堆:动态内存分配

堆是另一个存储数据的空间,JS 在其中存储对象和函数。

与堆栈不同,JS 引擎不会为这些对象分配固定数量的内存,而根据需要分配空间。这种分配内存的方式也称为动态内存分配。

下面将对这两个存储的特性进行比较:

堆栈
存放基本类型和引用
大小在编译时已知
分配固定数量的内存
对象和函数
在运行时才知道大小
没怎么限制

事例

来几个事例,加强一下映像。

  1. const person = { 
  2.   name'John'
  3.   age: 24, 
  4. }; 

JS 在堆中为这个对象分配内存。实际值仍然是原始值,这就是它们存储在堆栈中的原因。

  1. const hobbies = ['hiking''reading']; 

数组也是对象,这就是为什么它们存储在堆中的原因。

  1. let name = 'John'; // 为字符串分配内存 
  2. const age = 24; // 为字分配内存 
  3.  
  4. name = 'John Doe'; // 为新字符串分配内存 
  5. const firstName = name.slice(0,4); // 为新字符串分配内存 

始值是不可变的,所以 JS 不会更改原始值,而是创建一个新值。

JavaScript 中的引用

所有变量首先指向堆栈。如果是非原始值,则堆栈包含对堆中对象的引用。

堆的内存没有按特定的方式排序,所以我们需要在堆栈中保留对其的引用。我们可以将引用视为地址,并将堆中的对象视为这些地址所属的房屋。

请记住,JS 将对象和函数存储在堆中。基本类型和引用存储在堆栈中。

这张照片中,我们可以观察到如何存储不同的值。注意person和newPerson都如何指向同一对象。

事例

  1. const person = { 
  2.   name'John'
  3.   age: 24, 
  4. }; 

这将在堆中创建一个新对象,并在堆栈中创建对该对象的引用。

垃圾回收

现在,我们知道 JS 如何为各种对象分配内存,但是在内存生命周期,还有最后一步:释放内存。

就像内存分配一样,JavaScript引擎也为我们处理这一步骤。更具体地说,垃圾收集器负责此工作。

一旦 JS 引擎识别变量或函数不在被需要时,它就会释放它所占用的内存。

这样做的主要问题是,是否仍然需要一些内存是一个无法确定的问题,这意味着不可能有一种算法能够在不再需要那一刻立即收集不再需要的所有内存。

一些算法可以很好地解决这个问题。我将在本节中讨论最常用的方法:引用计数和标记清除算法。

引用计数

当声明了一个变量并将一个引用类型值赋值该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另外一个变量,则该值得引用次数加1。相反,如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减1。

当这个值的引用次数变成 0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。

我们看下面的例子。

请注意,在最后一帧中,只有hobbies留在堆中的,因为最后引用的是对象。

周期数

引用计数算法的问题在于它不考虑循环引用。当一个或多个对象互相引用但无法再通过代码访问它们时,就会发生这种情况。

  1. let son = { 
  2.   name'John'
  3. }; 
  4.  
  5. let dad = { 
  6.   name'Johnson'
  7.  
  8. son.dad = dad; 
  9. dad.son = son; 
  10.  
  11. son = null
  12. dad = null

由于父对象相互引用,因此该算法不会释放分配的内存,我们再也无法访问这两个对象。

它们设置为null不会使引用计数算法识别出它们不再被使用,因为它们都有传入的引用。

标记清除

标记清除算法对循环依赖性有解决方案。它检测到是否可以从root 对象访问它们,而不是简单地计算对给定对象的引用。

浏览器的root是window 对象,而NodeJS中的root是global。

该算法将无法访问的对象标记为垃圾,然后对其进行扫描(收集)。根对象将永远不会被收集。

这样,循环依赖关系就不再是问题了。在前面的示例中,dad对象和son对象都不能从根访问。因此,它们都将被标记为垃圾并被收集。

自2012年以来,该算法已在所有现代浏览器中实现。仅对性能和实现进行了改进,算法的核心思想还是一样的。

折衷

自动垃圾收集使我们可以专注于构建应用程序,而不用浪费时间进行内存管理。但是,我们需要权衡取舍。

内存使用

由于算法无法确切知道什么时候不再需要内存,JS 应用程序可能会使用比实际需要更多的内存。

即使将对象标记为垃圾,也要由垃圾收集器来决定何时以及是否将收集分配的内存。

如果你希望应用程序尽可能提高内存效率,那么最好使用低级语言。但是请记住,这需要权衡取舍。

性能

收集垃圾的算法通常会定期运行以清理未使用的对象。

问题是我们开发人员不知道何时会回收。收集大量垃圾或频繁收集垃圾可能会影响性能。然而,用户或开发人员通常不会注意到这种影响。

内存泄漏

在全局变量中存储数据,最常见内存问题可能是内存泄漏。

在浏览器的 JS 中,如果省略var,const或let,则变量会被加到window对象中。

  1. users = getUsers(); 

在严格模式下可以避免这种情况。

除了意外地将变量添加到根目录之外,在许多情况下,我们需要这样来使用全局变量,但是一旦不需要时,要记得手动的把它释放了。

释放它很简单,把 null 给它就行了。

  1. window.users = null

被遗忘的计时器和回调

忘记计时器和回调可以使我们的应用程序的内存使用量增加。特别是在单页应用程序(SPA)中,在动态添加事件侦听器和回调时必须小心。

被遗忘的计时器

  1. const object = {}; 
  2. const intervalId = setInterval(function() { 
  3.   // 这里使用的所有东西都无法收集直到清除`setInterval` 
  4.   doSomething(object); 
  5. }, 2000); 

上面的代码每2秒运行一次该函数。如果我们的项目中有这样的代码,很有可能不需要一直运行它。

只要setInterval没有被取消,则其中的引用对象就不会被垃圾回收。

确保在不再需要时清除它。

  1. clearInterval(intervalId); 

被遗忘的回调

假设我们向按钮添加了onclick侦听器,之后该按钮将被删除。旧的浏览器无法收集侦听器,但是如今,这不再是问题。

不过,当我们不再需要事件侦听器时,删除它们仍然是一个好的做法。

  1. const element = document.getElementById('button'); 
  2. const onClick = () => alert('hi'); 
  3.  
  4. element.addEventListener('click', onClick); 
  5.  
  6. element.removeEventListener('click', onClick); 
  7. element.parentNode.removeChild(element); 

脱离DOM引用

内存泄漏与前面的内存泄漏类似:它发生在用 JS 存储DOM元素时。

  1. const elements = []; 
  2. const element = document.getElementById('button'); 
  3. elements.push(element); 
  4.  
  5. function removeAllElements() { 
  6.   elements.forEach((item) => { 
  7.     document.body.removeChild(document.getElementById(item.id)) 
  8.   }); 

删除这些元素时,我们还需要确保也从数组中删除该元素。否则,将无法收集这些DOM元素。

  1. const elements = []; 
  2. const element = document.getElementById('button'); 
  3. elements.push(element); 
  4.  
  5. function removeAllElements() { 
  6.   elements.forEach((item, index) => { 
  7.     document.body.removeChild(document.getElementById(item.id)); 
  8.     elements.splice(index, 1); 
  9.   }); 

由于每个DOM元素也保留对其父节点的引用,因此可以防止垃圾收集器收集元素的父元素和子元素。

总结

在本文中,我们总结了 JS 中内存管理的核心概念。写这篇文章可以帮助我们理清一些我们不完全理解的概念。

希望这篇对你有所帮助,我们下期再见,记得三连哦!

作者:Ahmad shaded 译者:前端小智 来源:felixgerschau

原文:https://felixgerschau.com/javascript-memory-management/

本文转载自微信公众号「大迁世界」,可以通过以下二维码关注。转载本文请联系大迁世界公众号。

 

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

聊聊JavaScript内存管理

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

聊聊JavaScript内存管理

大多数时候,我们在不了解有关内存管理的知识下也只开发,因为 JS 引擎会为我们处理这个问题。不过,有时候我们会遇到内存泄漏之类的问题,这个只有知道内存分配是怎样工作的,我们才能解决这些问题。

聊一聊Linux内存管理

本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入到内核中讨论系统物理内存管理和内核内存的使用方法。力求从外到内、水到渠成地引导网友分析Linux的内存管理与使用。

聊一聊Lwip内存管理策略

LWIP中的内存池(POOL)分配策略简单,但是内存的分配、释放效率高,可以有效的防止内存碎片的产生。

聊聊操作系统的内存管理

brk()的作用也只是通知Linux内核哪个范围的堆内存是可用的,真正的物理内存页是在进程实际读写内存的时候才会申请,而且是由内核根据写时复制/需求加载自动完成的,应用程序感知不到这点。

聊一聊Redis内存使用和管理

本文从Redis数据库最关键的特性——内存出发,详细介绍了Redis的内存使用和内存管理,而这也是成为一名专业Redis运维人员的核心技能。

想和你聊聊操作系统的内存管理

在虚拟地址空间中,相邻的段所对应的物理内存空间可以不相邻,操作系统能够实现物理内存资源的离散分配,但是这种段式分配方式容易导致在物理内存上出现外部碎片。

聊一聊Redis内存碎片清理

翻看了Redis的相关资料发现,Redis4版本之后开始支持内存碎片的清理,于是进行了一次测试,内容如下。
Redis数据库2024-11-30

聊一聊Redis内存碎片处理

不知道我们在执行删除操作时有没有注意过这样一个现象,删除一些bigkey后内存分配器分配的容量并没有减少,实际容量减少了,这是为什么呢?

聊聊内存中的Slice操作

操作系统、处理器架构、Golang版本不同,均有可能造成相同的源码编译后运行时内存地址、数据结构不同。本文仅保证学习过程中的分析数据在当前环境下的准确有效性。

聊聊跨进程共享内存的内部工作原理

共享内存本质上共享的是内核对象 struct file,通过在不同的进程之间使用同一个 struct file 来实现的共享。当然也得需要在虚拟内存对象 vma 带上 VM_SHARED 标记来支持。

先聊聊「内存分配」,再聊聊Go的「逃逸分析」

通过本文的介绍,相信你一定加深了堆栈的理解;搞清楚逃逸分析的作用和原理之后能够指导我们写出更优雅的代码。

聊聊宿主机管理

为了解决这个问题,弹性云开发了宿主生命周期管理平台(mmachine)。通过不断优化和改进,mmachine 缩短了机器上线的耗时,提高了效率,并通过定制下线标准、并发漂移等方式,缩短了机器下线周期,在保障稳定性的前提前下,加速退还弹性伸

聊聊 Kube-Apiserver 内存优化进阶

Kube-apiserver 内存优化系系列包含前面的铺垫,到此也 6 篇了,如果把这其中涉及到的知识都搞懂了,对 kube-apiserver 的理解一定可以上一个台阶,后续也会持续关注这块的内容,不定时补充~
内存kube2024-11-30

聊聊C语言的内存分配

在标准C语言中,编译出来的可执行程序分为代码区(text)、数据区(data)和未初始化数据区(bss)3个部分。

一文聊聊Node中的内存控制

基于无阻塞、事件驱动建立的Node服务,具有内存消耗低的优点,非常适合处理海量的网络请求。在海量请求的前提下,就需要考虑“内存控制”的相关问题了。 1. V8的垃圾回收机制与内存限制 Js由垃圾回收机
2023-05-14

我们一起聊聊 Java 内存泄漏

使用Java编写程序时,我们使用new关键字创建对象。而且我们还不需要专门在对象使用完成后去释放其占用的内存,这是因为Java有专门的垃圾回收器来负责删除不需要的对象。

聊聊Java中的内存溢出问题

内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。这篇文章整理自《深入理解java虚拟机》。之前面阿里遇到过。

聊聊gitlab代码管理方法

gitlab代码管理方法随着软件开发的快速发展,代码管理越来越成为软件开发的一项关键工作。代码管理软件使团队能够更好地协作,跟踪项目,管理版本,并确保代码的可靠性和安全性。Gitlab是最受欢迎的代码管理软件之一,提供了一系列强大的功能和工
2023-10-22

聊聊Git 分支管理策略

Feature Toggle 是有成本的,不管是在加 Toggle 的时候的代码设计,还是在移除 Toggle 时的人力成本和风险,都是需要和它带来的价值进行衡量的。
Git分支管理2024-12-01

热门标签

编程热搜

编程资源站

目录