图文并茂讲清楚 JavaScript 内存管理

爱库里 等级 662 1 1

作为一个 JavaScript 的开发者,大多数情况下你可能不会担心内存管理问题,因为 JavaScript 引擎会帮你处理这些。但是在开发过程中,你或多或少的会遇到一些相关的问题,比如内存泄漏等,只有了解了内存分配的工作机制,你才会知道如何去解决这些问题。

在这篇文章中,我将会向你介绍 内存分配垃圾收集 的机制,以及如何避免一些 常见的内存泄漏 的问题。

内存生命周期

在 JavaScript 中,当我们创建变量、函数或者其他东西的时候,JS 引擎会自动的为它分配内存,当它不再被使用的时候,JS 引擎又会自动的去释放掉这块内存。

分配内存,实际上是在内存中保留一块空间的过程,而 释放内存 则是释放这块区域的空间,以便后续使用。

每次我们给变量赋值或者创建一个函数的时候,它所对应的那块内存总会经历如下的阶段:

图文并茂讲清楚 JavaScript 内存管理

js memory cycle

  • 内存分配

JavaScript 会帮我们处理,它会为我们创建的内容分配内存。

  • 内存使用

使用内存的过程体现在代码中,我们对于变量或对象等的读写其实就是对内存的读写。

  • 内存释放

这一步也是由 JavaScript 引擎处理的。一旦这个内存被释放掉了,它就可以用于新的目的。

堆内存和栈内存

现在我们知道了,在 JavaScript 中定义的任何东西,JS 引擎都会为他分配内存,并且在不再使用的时候释放掉。

接下来我们要考虑的问题就是:我们创建的变量、函数等,会被存放在哪里呢?

JavaScript 引擎有两个地方可以存储数据:堆内存栈内存

堆(Heap)栈(Stack) 是两种不同的数据结构,他们的使用场景也各不相同。

栈:静态内存分配

图文并茂讲清楚 JavaScript 内存管理

js stack memory

栈是 JavaScript 用来存放 静态数据 的一种数据结构。静态数据指的是 JS 引擎在编译时期就能确定其大小的数据。在 JS 中,它包括 原始的值(strings, numbers, booleans, undefined, symbol, and null)和 指向对象和函数的 引用

由于引擎知道了数据的大小不会再改变了,那么在分配内存的时候,就会给它分配一个 固定大小 的空间。

在程序执行前分配内存的过程,就叫做 静态内存分配

因为引擎为这些值分配的是固定大小的内存,所以这些值的大小肯定是有个上限的,而这个上限取决于具体的浏览器。

堆:动态内存分配

堆内存是 JavaScript 用来存在对象和函数的区域。与栈内存不同的是,引擎并不会为这些对象分配一个固定大小的内存,相反,它将根据具体的需要来分配对应的内存空间,这种内存分配的方式就是 动态内存分配

我们来对比一下栈和堆内存的区别:

| | 栈(Stack) | 堆(Heap) | | --- | --- | --- | | 值类型 | 原始值和引用 | 对象和函数 | | 时期 | 编译期间确定大小 | 运行期间确定大小 | | 大小 | 固定大小 | 无具体限制 |

例子

// 为对象分配堆内存  
const person = {  
  name: 'John',  
  age: 24,  
};  

// 数组也是对象,所以分配的也是堆内存  
const hobbies = ['hiking', 'reading'];  


let name = 'John'; // 为字符串分配栈内存  
const age = 24; // 为数字分配栈内存  

name = 'John Doe'; // 为字符串分配新的栈内存  
const firstName = name.slice(0,4); // 为字符串分配新的栈内存  

这里要注意的是,原始值都是不可变的,所以修改的时候实际上是创建了一个新的值。

JavaScript 中的引用

所有的变量一开始都是指向栈的。如果它不是原始值,那么栈中保留着指向堆内存中对象的引用。

堆内存里的数据并不是按照某个特定的顺序排列的,所以我们需要在栈中保留一个指向堆内存数据的引用。您可以将引用当作是地址,而堆内存中的对象则是这些地址所对应的房屋。

图文并茂讲清楚 JavaScript 内存管理

js heap pointers

上图清晰的展示了不同类型的值是如何存放的。要注意的是,personnewPerson 都是指向同一个对象的。

垃圾收集

这里已经知道了,JavaScript 会为所有类型的数据分配内存,但是如果你还记得一开始介绍的内存生命周期,你就知道我们还缺少最后一步:内存释放。

与内存分配一样,这一步也是由 JS 引擎为我们完成的,更具体的说,是 垃圾收集器 为我们完成的。

当 JS 引擎识别到给定变量或函数不再需要的时候,它就会释放其所占用的内存。

这一步骤的主要问题在于,我们无法精确的判定某一块内存是仍然需要的,这 只能是一个近似的过程,无法通过算法来解决。这里介绍两种最常见的算法:引用计数法 和 标记清除法(注意,它们也都是最大程度的近似判定)。

引用计数法

这是最简单的实现,它收集 没有引用指向它们的 对象作为垃圾。来看一下下面的演示:

图文并茂讲清楚 JavaScript 内存管理

reference-couting

这里要注意,在最后一帧中,只有 hobbies 保留在堆内存中,因为它是唯一一个有引用指向他的对象。

循环引用

引用计数法的问题在于,它没有考虑到循环引用的场景。当一个或多个对象之间相互引用,并且不能通过代码访问它们时,就会发生这种情况。看下面的例子:

let son = {  
  name: 'John',  
};  

let dad = {  
  name: 'Johnson',  
}  

son.dad = dad;  
dad.son = son;  

son = null;  
dad = null;  

图文并茂讲清楚 JavaScript 内存管理

reference cycle

由于 son 和 dad 这两个对象都引用了对方,所以这个算法不会释放它们占用的内存,我们也无法通过代码来访问到这两个对象。将它们都设置为 null 也无济于事,因为都有引用指向它们,所以标记清除法照样会认为它们是有用的,不可回收。

标记清除法

标记清除法很好的避免了循环引用的问题。它假定了一个叫做根(root)的对象,然后从它出发去访问给定的对象。根对象在浏览器中是 window 对象,在 NodeJS 中是 global 对象。

图文并茂讲清楚 JavaScript 内存管理

garbage-collectoion-algorithm

该算法将 不可访问的对象 标记为垃圾,然后 清除(收集)它们。根对象将永远不会被收集。这样,循环引用就不再是个问题了。在之前的例子中,dad 和 son 这两个对象最后都无法通过根对象访问到,所以它们都会被标记为垃圾然后被清理掉。

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记-清除算法性能和实现的改进,并不是对算法本身。

权衡

自动的垃圾收集机制让我们可以专注于构建应用程序本身,而不用因为内存管理而浪费时间。然而,我们需要注意一些权衡。

内存使用

由于算法无法确切的知道何时不再需要某块内存,所以 Javascript 应用可能会比平时需要更多的内存

即使某些对象已经被标记为垃圾了,但具体的垃圾收集时机还是由垃圾收集器来决定的。

如果你想你的应用程序尽可能地提高内存效率,那你最好使用一些底层(lower-level)语言。但请记住,任何语言的内存管理都有自己的一套权衡。

性能

为我们收集垃圾的算法通常是定期运行的。然而问题是,作为开发者,我们并不知道它什么时候发生。收集大量的垃圾或频繁地收集垃圾可能会影响性能,因为这样做需要一定的计算能力。当然,我们的用户或开发人员通常不会注意到这种影响。

内存泄漏

好了,有了上面的知识储备,下面我们来看看几种常见的内存泄漏问题。当你理解背后的原理时,你就会发现这些问题都可以轻松的避免。

全局对象

将数据存储在全局变量上可能是最常见的内存泄漏问题了。举个例子,在浏览器中声明一个变量,如果你不用 const 或者 let,而是用 var 或者干脆省略关键字,那么这个变量将会变成 window 对象的一个属性。用 function 定义的函数也同理。

major = 'JS';  
var user = 'Jerry';  
function getName() {  
  return 'jerry';  
}  

window.major // => 'JS'  
window.user // => 'Jerry'  
window.getName() // => 'jerry'  

这只适用于在全局作用域中定义的变量和函数,关于 JS 作用域的内容你可以参考这篇文章。

你可以在 严格模式 下运行你的代码,这样可以避免上述问题。

当然有时候你可能是故意的使用全局变量来存储一些信息,但是请确保在不再需要这些对象的时候主动的设置为 null,这样可以保证垃圾收集器可以及时的回收掉它的内存:

window.user = null;  

被遗忘的定时器与回调函数

忘记处理了某些计时器和回调函数会增加应用程序的内存。特别是在单页应用程序(SPA)中,在动态添加事件监听和回调时务必要小心。

定时器

const object = {};  
const intervalId = setInterval(function() {  
  doSomething(object);  
}, 2000);  

这段代码每两秒执行一次,定时器内部引用了外部的 object 对象。只要定时器在运行,这个 object 对象就不会被回收。所以要确保在合适的时机清除掉这个定时器:

clearInterval(intervalId);  

这点在 SPA 中特别重要。因为有时候你可能已经导航到另一个页面去了,但是原先页面的定时器还在后台运行着,它导致了引用了外部对象无法被回收。

回调函数

假设你有一个按钮,它绑定了一个 onclick 事件。

一些老的浏览器的垃圾回收器是无法收集监听器的,不过现在基本都可以了,不过还是建议你在不需要的时候,手动的移除事件监听,释放内存。

const element = document.getElementById('button');  
const onClick = () => alert('hi');  

element.addEventListener('click', onClick);  

element.removeEventListener('click', onClick);  
element.parentNode.removeChild(element);  

DOM 引用

这种内存泄漏与上一个相似,它们都发生在存储 DOM 元素的时候。

const elements = [];  
const element = document.getElementById('button');  
elements.push(element);  

function removeAllElements() {  
  elements.forEach((item) => {  
    document.body.removeChild(document.getElementById(item.id))  
  });  
}  

当你删除某一个元素的时候,你可能希望从 elements 数组中也删除对应的元素。否则,这些 DOM 元素还是不能被垃圾收集器收集。

const elements = [];  
const element = document.getElementById('button');  
elements.push(element);  

function removeAllElements() {  
  elements.forEach((item, index) => {  
    document.body.removeChild(document.getElementById(item.id));  
    // 从数组中删除  
    elements.splice(index, 1);  
  });  
}  

参考文档

本文转自 https://mp.weixin.qq.com/s/uk75AoNVSuvzTrImJ-KhMA,如有侵权,请联系删除。

收藏
评论区

相关推荐

js去除字符串
js去除字符串js<DOCTYPE html<html<head <title</title</head<body</body<script type"text/javascript" function delHtmlTag(str){   return str.replace(/<^/g,""); } var s
javascript实践教程-02-javascript入门
本节目标1. 掌握如何编写javascript代码。2. 掌握javascript的3个弹框。3. 掌握javascript的注释。4. 掌握浏览器的调试工具控制台。 内容摘要本篇介绍了如何在网页上编写js代码,如何引入外部js代码文件,js的3个弹框、注释语法,还有浏览器调试工具的控制台使用。阅读时间1520分钟。 script标签如果我们需要在网页中编写
JSON学习笔记(二、语法)
#### JSON和js关系 欲学JSON先学js,那么JSON和js的关系是什么样的呢? .JSON 使用 JavaScript 语法来描述数据对象,但是 JSON 仍然独立于语言和平台。 .JSON 语法是 JavaScript 语法的子集 #### 基本语法 .数据在名称/值对中 .数据由逗号分隔 .大括号保存对象 .中括号保
JS导出页面为PDF文件,该如何操作?来看一眼就明白啦!
废话不多说,直接上代码。 1.资源文件或依赖 <script type="text/javascript" src="https://my.oschina.net//u/4265132/blog/4054317/js/canvg2.js"></script><script type="text/javascript" src="https://m
JS的常用属性
JS-------定义:基于事件和对象驱动,并具有安全性能的脚本语言。 引入:<script  type=”text/javascript”>具体js代码</script> <script  type=”text/javascript” src=”js文件”></script> 大小写敏感:例如:A与a是两个不同的东东 注释://  单
java中ajax的用法简单案例
1.index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <script type="text/javascript" src="js/jquery-2.1.0.js">
008_Node中的require和import
一、js的[对象的解构赋值](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FReference%2FOperators%2FDestructuring_assignment)
D3.js selectAll()函数与enter()函数
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>D3 Test</title> <script type="text/javascript" src="d3.v3.js"></script> </head> <body>
Html5添加三联切换仿3D效果旋转木马jQuery插件教程
一、使用方法 <script type="text/javascript" src="js/jquery.min.js"></script> <script type="text/javascript" src="js/gallery.js"></script>    二、Html结构 <div id="wrapper"> <div id="mai
JQValidate使用说明
JQuery Validate使用总结: 一、导入js库 <script src="../js/jquery.js" type="text/javascript"></script> <script src="../js/jquery.validate.js" type="text/javascript"></script> 二、默认校验规则
JQuery
一、jq简介 jq其实就是js的一个文件。 二、jq书写步骤 1、先引入jq文件(min的文件) <script type="text/javascript" src="file:///C|/jquery/jquery-3.3.1.min.js"></script> 2、换新的一行写js代码 <script type="text/javascri
JavaScript prototype原型用法
JavaScript对象原型 -------------- 所有JavaScript对象都从原型继承属性和方法。 <!DOCTYPE html> <html> <meta charset="utf-8"> <title>js</title> <body> <h2>JavaScript 对象</h2
JavaScript基础知识
**1:从一个界面跳转到其他界面** window.location.href="b.html" **2:判断变量是否为空** typeof obj == 'undefined' || obj == null || obj == "" **3:html引用公用的css和js** <script type="text/javascript" src=
JavaScript的入门简介
#### 什么是 JavaScript JavaScript,我们一般简称为 JS,是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。 JavaScript 现在已经被用到了很多非浏览器环境中,JavaScript 基于原型编程、多范式的动态脚本语言,并支持面向对象、命令式和声明式风格。 HTML、CSS、JavaScript三者不同的功能:
Springmvc异步上传文件
<script src="js/jquery.js" type="text/javascript"></script><script src="js/jquery.ext.js" type="text/javascript"></script><script src="js/jquery.form.js" type="text/javascript"

热门文章

Kubernetes笔记:十分钟部署一套K8s环境部署Go语言项目的 N 种方法

最新文章

部署Go语言项目的 N 种方法Kubernetes笔记:十分钟部署一套K8s环境