JS(Memory Management) 内存管理

简介: • 1.内存的生命周期• 2.堆和栈• 栈:静态内存分配• 堆:动态内存分配• 示例• 3.JS中的引用• 示例• 4.垃圾回收• 引用计数• 标记清除• 5.内存泄漏• 将变量挂载到全局变量• 关闭定时器和清除回调函数• 清除DOM 引用


该文章是以国外一篇文章,关于JS 内存管理(JavaScript's Memory Management Explained)为基础。同时加上其他资料的所编写的。

如果想直接根据原文学习,可以忽略此文。但是不要忘记点赞+关注。

如果你觉得可以,请多点赞,鼓励我写出更精彩的文章🙏。
如果你感觉有问题,也欢迎在评论区评论,三人行,必有我师焉


该文章为JS如何在浏览器中运行系列

概要


内存的生命周期

在JS中,无论新建一个变量,函数或者其他数据结构,JS引擎会默默的为我们做许多事-为对应的数据结构分配(allocate)内存空间并且在该数据不在被需要的时候回收(deallocate/release)它。

如果大家在对底层语言(C/C++)比较熟悉的话,在C++中利用标准库,allocate / deallocate 就可以实现内存的手动分配和回收

分配内存是预留内存的过程,而回收内存是将被占用的内存回收,为其他操作提供空间。

就像人有生老病死,内存也有对应的生命周期。每当赋值变量或者创建函数,内存都会经历如下的过程:

  • 分配内存
    JS引擎为我们分配我们需要操作对象的对应空间
  • 使用内存
    对内存的读写,只不过是对变量的读写
  • 回收内存
    JS引擎通过特定的算法判断内存是否需要释放,而被释放的内存用于为其他操作提供空间。
在内存管理器中的对象不仅仅包括JS对象,而且还囊括了函数和函数作用域。 JS中一切皆对象

堆和栈

我们从上文得知,JS引擎分配空间并在该空间不被引用的时候将其回收

那这些被分配的空间具体被存放在哪里呢?

JS引擎有两个地方用于存放数据:(memory heap)和(stack)。

是JS引擎存放数据的两个不同的数据结构

栈:静态内存分配

上图,所有的值都被存放到 stack 中-->由于他们的类型都是原始类型(primitive)

stack 是JS用于存放静态数据的数据结构。静态数据是一种JS引擎在编译阶段能准确知道该数据大小的数据类型。在JS中,静态数据包括原始类型string/number/boolean/undefined/null)和引用类型(指向对象和函数的指针)。

由于JS引擎知道它们的大小不会发生更改,所以每次都会为其分配指定大小的内存空间。

在代码执行之前分配内存的过程被称为 静态内存分配

由于JS引擎为这些数据分配定值的内存空间,所以在stack存储的数据是有内存上限的。而该上限由不同浏览器各自决定。


堆:动态内存分配

heap 是JS用于存放对象函数的地方。

不像stack,JS引擎不会为这些对象分配定值的内存空间。相反,这些空间是按需分配的。

该分配内存的方式被称为动态内存分配

为了便于区分stackheap各自的区别,绘制如下的表格:

区别 Stack Heap
存储类型 原始类型和引用类型 对象和函数
内存是否定值 编译阶段已经确定内存大小 运行阶段确定大小
存储数据是否存在内存上限 是(不同浏览器规则不同) 不存在内存上限

示例

const person = {
  name: 'John',
  age: 24,
};
复制代码

JS为这个对象在heap中分配空间。然而其属性的值为原始类型,所以属性值被存储在stack中。

const hobbies = ['hiking', 'reading'];
复制代码

数组也属于对象,所以它也是被存储在heap中。

let name = 'John'; // 为string 分配内存
const age = 24; // 为数字分配内存
name = 'John Doe'; // 重新分配内存
const firstName = name.slice(0,4); // 重新分配内存
复制代码

原始数据是不可变immutable)的。为了将原来的值进行替换,JS会重新创建一个新的值。


JS中的引用

所有变量都在stack中存在指定的信息。 在上文中我们得知,stack中存储两种类型的值- 原始类型引用类型。 原始类型我们不用过多解释,而引用类型需要着重解释一下。

由于对象和函数存放在heap中,而heap是一个杂乱无章的数据结构,没有指定的顺序,所以JS解释器在从上到下编译代码的时候,就会按照数据出现的顺序,依次按照数据类型存放到指定的位置。而在发现某个数据是非原始类型,就会在stack中存储其在heap中存储的引用地址。


垃圾回收

通过上文的学习,我们已经知道了JS引擎是如何存储不同的数据,但是凡事都是有头有尾的,既然存在内存的分配,那势必就会存在内存的回收。

和内存分配一样,内存回收的工作JS也为我们代劳了。并且还派专人(GC (garbage collector))全权负责此事。

一旦JS引擎发现变量或者函数不在被引用GC将其占用的空间释放(deallocate/release)掉。

而回收内存最主要的问题是,内存是否被引用是一个不可预知的事,这也意味着没有一种算法能够在内存不被占用的时候,将所有的内存回收。

下面我们讨论一些比较典型的垃圾回收算法。

引用计数

这是一种最简单的方式。它通过回收那些没有指针指向的对象。

通过上面的一系列操作,虽然将 personnewPerson的引用都给置为 null,但是对象中 hobbies的还是被其他变量所引用。

循环引用

针对引用计数这种GC方式,有一种情况是无法处理的-循环引用。当一个或者多个对象互相引用,但是这些对象已经处于孤立状态。此时,引用计数就手足无措了。

let son = {
  name: 'John',
};
let dad = {
  name: 'Johnson',
}
son.dad = dad;
dad.son = son;
son = null;
dad = null;
复制代码

虽然在不使用sondad 的时候,将它们都置为null。这只是将它们与stack中的引用脱离的关系,但是在heap中,他们还彼此引用。

sondad 对象互相引用,而引用计数的算法在这两个对象没有被其他对象引用的时候是无法释放内存的。


标记清除

标记清除算法能够很好的解决循环引用的痛点。不同于引用计数通过计算对象引用个数的方式来决定是否进行垃圾回收,它采用了一种通过从根对象开始遍历,如果某个对象遍历不到,那就需要被GC释放对应的内存。

在浏览器中,这个根对象window对象,而在NodeJS中,是global对象。

该算法通过 标记那些不能被访问到的对象,作为GC的目标。 根元素不参与标记过程!

内存泄漏

通过上文的介绍和对已有概念的掌握,我们来分析一些常规的可能造成内存泄漏的原因。

将变量挂载到全局变量

将数据肆无忌惮的存储在全局变量上,这是一种很常见的内存泄漏。

在浏览器环境中,如果在定义一个变量的时候,缺省了var/const/let,此时定义的变量就会被挂载到window对象上。

users = getUsers();
复制代码

我们可以通过以strict mode来运行代码。

如果在某些情况下,逼不得已需要将变量挂载到全局变量上,你需要在该变量使命完成的时候,手动将其置为null

window.users = null;
复制代码

关闭定时器和清除回调函数

忘记清除定时器和回调函数,将会使项目的代码越来越大。尤其针对SPA(单页面应用),在动态添加定时器和回调的时候要格外小心。

及时关闭定时器

const object = {};
const intervalId = setInterval(function() {
  // everything used in here can't be collected
  // until the interval is cleared
  doSomething(object);
}, 2000);
复制代码

上面的代码,每2m执行一次,定时器中的变量在定时器没有关闭的时候,是一直无法被GC的。

所以,在不使用定时器的时候,需要将其销毁。

clearInterval(intervalId);
复制代码

清除回调函数

const element = document.getElementById('button');
const onClick = () => alert('hi');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
复制代码

清除DOM 引用

将DOM元素存储到JS中,可能也会导致内存泄漏。

当你想通过数组来销毁元素,这种情况是无法触发GC操作的

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);
  });
}
复制代码



相关文章
|
1月前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
1月前
|
JavaScript 前端开发 Java
避免 JavaScript 中的内存泄漏
【10月更文挑战第30天】避免JavaScript中的内存泄漏问题需要开发者对变量引用、事件监听器管理、DOM元素操作以及异步操作等方面有深入的理解和注意。通过遵循良好的编程实践和及时清理不再使用的资源,可以有效地减少内存泄漏的风险,提高JavaScript应用程序的性能和稳定性。
|
25天前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
176 77
|
25天前
|
监控 JavaScript
选择适合自己的Node.js内存监控工具
选择合适的内存监控工具是优化 Node.js 应用内存使用的重要一步,它可以帮助你更好地了解内存状况,及时发现问题并采取措施,提高应用的性能和稳定性。
115 76
|
25天前
|
监控 JavaScript 数据库连接
解读Node.js内存监控工具生成的报告
需要注意的是,不同的内存监控工具可能会有不同的报告格式和内容,具体的解读方法可能会有所差异。因此,在使用具体工具时,还需要参考其相关的文档和说明,以更好地理解和利用报告中的信息。通过深入解读内存监控报告,我们可以不断优化 Node.js 应用的内存使用,提高其性能和稳定性。
100 74
|
27天前
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
116 62
|
28天前
|
监控 JavaScript Java
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
122 52
|
23天前
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
66 31
|
23天前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
39 3
|
28天前
|
缓存 监控 JavaScript
避免在Node.js中出现内存泄漏
总之,避免内存泄漏需要在开发过程中保持谨慎和细心,遵循最佳实践,不断优化和改进代码。同时,定期进行内存管理的检查和维护也是非常重要的。通过采取这些措施,可以有效地降低 Node.js 应用中出现内存泄漏的风险,确保应用的稳定和性能。