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

爱库里 等级 373 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排序算法
引子 有句话怎么说来着: 雷锋推倒雷峰塔,Java implements JavaScript. 当年,想凭借抱Java大腿火一把而不惜把自己名字给改了的JavaScript(原名LiveScript),如今早已光芒万丈。node JS的出现更是让JavaScript可以前后端通吃。虽然Java依然制霸企业级软件开发领域(C/C 的大神们不要打
web性能优化的15条实用技巧
javascript在浏览器中运行的性能,可以认为是开发者所面临的最严重的可用性问题。这个问题因为javascript的阻塞性而变得复杂,事实上,多数浏览器使用单一进程来处理用户界面和js脚本执行,所以同一时刻只能做一件事。js执行过程耗时越久,浏览器等待响应的时间越长。 加载和执行 1.提高加载性能 1.IE8,FF,3.5,Safari 4和
JavaScript基础加ES6语法
JavaScript 一、什么是JavaScript 当下最流行的脚本语言,在世界上的所有浏览器中都有js的身影,是一门脚本语言,可以用于我们与web站点和web应用程序的交互,还可以用于后台服务器的编写,例如node.js 二、语法特点 基于对象和事件驱动的松散型,解释型语言 单线程异步 三、JavaScript作用 页面的交
js 惰性载入函数( 能力检测 )
今天在做项目时,需要对地址栏参数做处理,于是便作了如下处理 javascript getUrlParam() { let params {}; const url window.location.href.replace(/\//g,'').replace(/\?/g,'&'); url.replace(/?&(^&)
几个常用js库,别再重复造轮子了
年底了,总结下今年用到的一些有意思的《js轮子》(只是大概列出些比较有意思的库,每个标题都是超链接,可点击自行查阅) 希望能对您有用! 如有意思的 轮子 可以在评论列出一起讨论下 color(https://links.jianshu.com/go?tohttps%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fco
前端面试题自检 JS CSS 部分
JS类型 JavaScript的简单数据类型Number , String , Boolean , Undefined , Null , Symbol typeof 操作符的返回值 number string boolean undefined object function
js异步的5种样式
js异步的5种样式 1.定时器 2.AJAX 3.Promise 4.Generator 5.asyns和awite  1.定时器     setTimeout() : 延时器        可以传入三个分别是            1)code:必
js-Answers一
JavaScript的组成 JavaScript 由以下三部分组成: 1. ECMAScript(核心):JavaScript 语言基础 2. DOM(文档对象模型):规定了访问HTML和XML的接口 3. BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法 JS的基本数据类型和引用数据类型
「组件」返回顶部按钮
样式如图1:在components文件夹下新建BackTop.vue js<template <div class"backTopBtn" <a href"javascript:;" <div vif"btnFlag" class"btn" @click"backTop"TOP</div
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
js删除表格中的某一行
点击表格中的内容,删除某一行正文js代码如下 function removeTd(obj) { obj.parentNode.parentNode.remove();}
只听说过CSS in JS,怎么还有JS in CSS?
CSS in JS是一种解决css问题想法的集合,而不是一个指定的库。从CSS in JS的字面意思可以看出,它是将css样式写在JavaScript文件中,而不需要独立出.css、.less之类的文件。将css放在js中使我们更方便的使用js的变量、模块化、treeshaking。还解决了css中的一些问题,譬如:更方便解决基于状态的样式,更容易追溯依赖关
React - Fiber原理
浏览器渲染 屏幕刷新率(FPS) 浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿 绘制一次的称为一帧,平均每帧16.6ms 帧 每个帧的开头包括样式计算、布局和绘制 js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的 如果某个js任务执行时间过长,浏览器会推迟渲染,每
面试官:JavaScript的数据类型你了解多少?
前言作为JavaScript的入门知识点,Js数据类型在整个JavaScript的学习过程中其实尤为重要。最常见的是边界数据类型条件判断问题。我们将通过这几个方面来了解数据类型: 概念 检测方法 转换方法 概念undefined、Null、Boolean、String、Number、Symbol、BigInt为基础类型;Ob
从 生成器 到 promise+async
本文主要讲解js中关于生成器的相关概念和作用,以及到后面结合 promise 实现 es7中的 async 原理,你将学习到js中异步流程控制相关知识 1、认识生成器思考如下代码:javascript let x 1 function foo() x++ bar() console.log(x) // 3 function bar(

热门文章

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

最新文章

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