一文读懂 JS 内存管理,掌握面试中七大内存泄漏场景

开发 前端
JavaScript 与许多现代编程语言一样,使用自动内存管理,即:JS 会自动完成垃圾回收。这也意味着,我们在程序运行时无需手动分配和释放内存(这也是很多同学很少关注它的原因)。

说起内存管理,可能是很多同学既熟悉、又陌生的一个领域。熟悉是因为我们经常会听到这个词,陌生是因为好像我们在开发时又很少去关注它。

不过,在目前的前端面试中,内存管理的问题被问的又比较频繁。所以,咱们今天就来看看 JS 内存管理的那些事...

JavaScript 如何管理内存

JavaScript 与许多现代编程语言一样,使用自动内存管理,即:JS 会自动完成垃圾回收。这也意味着,我们在程序运行时无需手动分配和释放内存(这也是很多同学很少关注它的原因)。

自动内存管理(垃圾回收)主要通过 标记-清除 法完成,大致分为两个阶段:

  1. 标记阶段:垃圾收集器从根对象开始,标记所有可到达或可访问的对象。
  2. 清除阶段:任何未标记(即可无法访问)的对象都被视为“垃圾”,并且其内存将被释放。

但是,自动的内存管理毕竟无法解决所有场景下的内存问题,所以在某些特殊情况下就会导致出现 内存泄漏 的问题。

而这个 内存泄漏 就是我们在面试中聊到内存管理时的,重点描述 内容!

关于【内存泄漏】

垃圾回收经常会伴随着内存泄漏的概念。不过我们需要知道的是 内存泄漏 和 内存溢出 是不同的。

PS 内存溢出指的是:程序运行过程中需要使用的内存大于系统能够提供的内存。即:系统内存不足。

所谓的内存泄漏指的是:当一块内存(通常是变量)未被释放(垃圾回收),同时我们也无法访问到它时。大家可以简单理解为:一个变量,我们访问不到它了,但是它占用的内容还没有被回收。那么此时就会出现内存泄漏的问题:

1. 未清理的定时器或回调

  • 场景:使用 setInterval 或 setTimeout,但在组件销毁或不再需要时没有清理。
  • 结果:定时器依然存在,引用被保留,导致无法释放内存。
  • 示例:
function startTimer() {
   setInterval(() => {
       console.log('...');
   }, 1000);
}
// 如果调用此函数后不清理定时器,则会造成内存泄漏
  • 修复:
const timer = setInterval(() => {
    console.log('Timer running...');
}, 1000);
clearInterval(timer); // 组件销毁时清理

2. DOM 引用未释放

  • 场景:保留对已移除 DOM 元素的引用。
  • 结果:虽然 DOM 节点从页面上被删除,但 JavaScript 中仍有对它的引用,导致内存泄漏。
  • 示例:
let element = document.getElementById('leak');
document.body.removeChild(element);
// 仍然引用 element,导致内存无法释放
  • 修复:
element = null; // 手动清除引用

3. 闭包中的多余引用

  • 场景:闭包内的变量保留了对外部对象的引用,导致对象无法被垃圾回收。
  • 结果:长时间的引用链阻止了内存的释放。
  • 示例:
function createClosure() {
    const largeObject = new Array(1000).fill('leak');
    return function() {
        console.log(largeObject);
    };
}
const closure = createClosure();
// 即使 closure 不再使用,largeObject 依然存在
  • 修复:
function createClosure() {
    let largeObject = new Array(1000).fill('leak');
    return function() {
        console.log(largeObject);
        largeObject = null; // 主动清除引用
    };
}

4. 全局变量或未声明变量

  • 场景:未使用 var, let, 或 const 定义变量,导致变量挂载在全局作用域。
  • 结果:全局变量生命周期与页面一致,无法被垃圾回收。
  • 示例:
function leak() {
    leakedVariable = 'I am a leak'; // 隐式全局变量
}
leak();
  • 修复:
function noLeak() {
    let localVariable = 'I am safe'; // 使用局部作用域变量
}

5. 事件监听器未清理

  • 场景:在 DOM 元素上绑定了事件监听器,但没有在元素销毁时移除。
  • 结果:监听器仍然存在,阻止 DOM 元素被回收。
  • 示例:
const button = document.getElementById('myButton');
button.addEventListener('click', () => console.log('点击!'));
document.body.removeChild(button);
// 内存泄漏:button 和监听器仍被引用
  • 修复:
const button = document.getElementById('myButton');
const handler = () => console.log('点击!');
button.addEventListener('click', handler);
button.removeEventListener('click', handler); // 组件销毁时移除
document.body.removeChild(button);

6. 忘记清理 Map/Set 中的键或值

  • 场景:Map 或 Set 中的键/值对象未手动移除。
  • 结果:即使这些对象不再需要,也会因其被引用而无法回收。
  • 示例:
const map = new Map();
const obj = { key: 'value' };
map.set(obj, 'data');
// 即使 obj 不再使用,map 仍然引用它
  • 修复:
map.delete(obj); // 手动清理不需要的键

7. 闭环引用

  • 场景:两个对象互相引用,导致垃圾回收无法判断它们是否可释放。
  • 结果:循环引用造成内存泄漏。
  • 示例:
function CircularReference() {
    const obj1 = {};
    const obj2 = {};
    obj1.ref = obj2;
    obj2.ref = obj1;
}
  • 修复:
// 如果可能,避免互相引用,或通过 WeakMap/WeakSet 管理对象
const obj1 = new WeakMap();
const obj2 = {};
obj1.set(obj2, 'value');

责任编辑:武晓燕 来源: 程序员Sunday
相关推荐

2021-04-30 19:53:53

HugePages大内存页物理

2021-05-12 18:22:36

Linux 内存管理

2021-04-24 09:02:36

Linux 内存分配

2021-06-30 08:45:02

内存管理面试

2019-04-15 13:11:16

LoRaWAN物联网网络

2020-09-14 15:43:53

内存SDRAMDDR4

2021-05-14 12:21:04

ERP业务需求CIO

2022-03-03 11:47:17

参数时序频率

2020-01-14 12:08:32

内存安全

2017-03-20 13:43:51

Node.js内存泄漏

2017-03-19 16:40:28

漏洞Node.js内存泄漏

2024-03-26 00:33:59

JVM内存对象

2018-05-31 20:49:50

Spark堆内内存优化机制

2022-05-04 17:43:28

元数据大数据

2022-06-16 08:01:06

云成本管理FinOps

2021-08-09 09:54:37

内存泄漏JS 阿里云

2023-12-22 19:59:15

2021-08-04 16:06:45

DataOps智领云

2021-01-15 18:15:27

人工智能AI

2021-12-20 07:59:07

Go语言结构体
点赞
收藏

51CTO技术栈公众号