JS中的内存管理是怎么工作的?

AI 概述
JavaScript 采用自动内存管理,数据存于栈(原始值)和堆(对象等)中,垃圾回收器用标记 - 清除算法回收不再需要的内存,现代引擎还采用分代垃圾回收优化。但也可能出现内存泄漏,如意外全局变量、遗忘定时器和回调、闭包持引用、脱离 DOM 元素、未移除事件监听器等。开发者可借助 Chrome DevTools 等工具监控内存使用。了解这些机制和问题,有助于避免内存堆积,让程序更高效稳定运行。
目录
文章目录隐藏
  1. 自动内存管理
  2. 内存分配
  3. 常见的内存泄漏
  4. 监控内存使用情况
  5. 结语

JS 中的内存管理是怎么工作的?

本文是对 JavaScript 里的内存管理机制做个简单又快速的回顾和提醒。

内存管理这事儿,对于 JavaScript 开发者来说,就像是个“隐形”的家伙,平时很少会主动去关注它——除非它出问题了,我们才会突然意识到:“哦,原来还有这么个东西在背后捣鼓呢!”和 C 或者 C++ 这些底层语言可不一样,在那些语言里,你得自己动手去分配和释放内存,但在 JavaScript 里,这些事儿它都自动帮你搞定啦。

一个快速类比:内存就像你填满的图书馆书架当你创建一个变量时,就像往书架上放了一本新书。图书管理员(也就是 JavaScript 引擎)会找到一个空位,并记录这本书放在哪儿(即分配内存)。随着时间推移,如果你不再引用这本书——没有人来阅读或使用它——图书管理员最终会注意到它只是积灰占地。所以,在清理时间到来时,他们会把这本书从书架上移除,为新书腾出空间。这就是 JavaScript 的做法:它会自动找出那些未被使用的“书”(数据),并将其清除,以防你的图书馆溢出。

自动内存管理

JavaScript 采用自动内存管理机制,也就是说,它会自动为你分配和释放内存。

当你:

  • 创建一个值 → 内存被分配;
  • 使用该值 → 这块内存保持活跃;
  • 放弃该值 → 内存最终会被垃圾回收机制释放。

所以,虽然你不需要手动“归还”内存,但了解 JavaScript 是在何时、如何处理内存的,有助于你避免无意间造成的内存堆积。

内存分配

JavaScript 主要将数据存储在两个地方:栈(stack)和堆(heap)。

JS 中的内存管理是如何工作的

栈内存(简单、固定大小的值)

原始值(primitive values)——比如 number、string 和 boolean——会被存放在栈中。

栈是一块小而整洁的内存区域,按照“后进先出”(LIFO)的顺序存储数据。

你可以将它想象成自助餐厅里的一摞托盘:只能从顶部添加或取出。它结构有序、速度快,非常适合存放体积小、大小固定的值。

let x = 42;
let y = "hello";

当包含这些值的函数或代码块执行完毕后,栈会直接将这些值弹出——快速且高效。

堆内存(动态、复杂的值)

对象、数组和函数会被存储在堆中,也就是一个更大、更灵活的内存区域。

你可以将堆想象成一个大型仓库,里面的物品大小和形状各不相同。当你创建一个对象时,JavaScript 会将实际的对象存放在仓库(堆)中,并在栈上放置一张便签(引用),标记它在堆中的位置。

let user = { name: "Alice" }; // 栈中存放引用,堆中存放数据
let numbers = [1, 2, 3];

由于堆中的数据可能会增长或缩小,查找和清理它们的过程会更耗时——但这也赋予了 JavaScript 处理复杂数据的灵活性。

JS 中的内存管理是怎么工作的?

垃圾回收:清理小队

当某块内存不再被需要时,垃圾回收器(Garbage Collector,简称 GC)会自动回收它。

可以把你的程序想象成一棵巨大的树。树干代表全局作用域(global scope),而与它连接的每一根树枝或树叶,代表一个处于活动状态的变量、对象或引用。

随着程序运行,一些树枝会“断开”——比如当你将一个对象设为 null,或者函数执行结束,其内部变量超出作用域。这些断开的树枝(不可达对象)就不再属于这棵“活着”的树。

垃圾回收器的工作就像大自然一样:它会定期检查这些掉落的树叶(不可达对象),将它们清理掉,并回收“养分”(内存),以便新叶子生长。

标记-清除算法

最常用的垃圾回收算法叫做标记-清除(mark-and-sweep)。

  • 标记阶段(Mark phase):GC 从“根”引用(全局变量和活动函数范围)开始,标记所有能从这些根引用访问到的内存为“正在使用”。
  • 清除阶段(Sweep phase):然后,它遍历整个内存,将那些未被标记的内容移除,从而释放空间。

JS 中的内存管理是怎么工作的?

还记得我们一开始用来类比堆(heap)的比喻吗?没错——一个装满各种箱子的大型仓库,有些箱子里是有用的物品(对象),有些则是废弃杂物。我们继续沿用这个比喻来解释。

1. 初始状态(图片左侧)

  • “Objects”(蓝色圆形):这些是当前存在于内存中的箱子(数据或变量)。其中一些箱子之间是相互连接的(用箭头表示),意味着一个箱子可能包含对另一个的引用。
  • “Reachable Objects”(浅蓝色方框):这些表示内存中可能被占用的部分,但目前并不一定由活跃对象使用。
  • Roots(隐含的):可以想象你的程序中有一些特殊的“起始点”,比如全局变量或当前正在执行函数中的变量。这些就是所谓的“根”(roots)。从这些根出发能访问到的任何对象,都会被认为是重要的。

2. 标记阶段(图片中间部分)

  • “标记”过程:垃圾回收器(GC)就像一个侦探,它从这些“根”出发,在内存房间里巡视。
  • “只要能找到,它就还活着!” 从根开始,GC 会沿着引用关系(箭头)查找内存中的对象。凡是能通过连接找到的对象,都会被“标记”或“打标签”为当前正在使用(用绿色圆圈和绿色箭头表示)。这就像是在说:“这个箱子以及与它相连的所有内容都很重要,不能扔!”
  • 未标记的对象(灰色圆圈):那些侦探从根出发无法抵达的箱子不会被标记。它们就是被认为没人再需要的“废弃”箱子。

3. 清除阶段(图片右侧)

  • “清理”过程:标记完成后,GC 会再次遍历整个内存空间。
  • “旧的清出去!” 所有在上一阶段中未被标记的箱子(灰色圆圈)都会被视为垃圾。GC 会将它们移除,释放出对应的内存空间。
  • “已回收的空闲内存”(虚线框):这些是原先垃圾箱子所在的位置。现在它们已经被清空,可以用于存放新的数据。
let person = { name: "Charlie" };
person = null; // 现在变为不可达
// GC 最终会释放这块内存

另一个类比,标记-清除算法就像整理储物间的过程:

  • 标记(Mark):找出你当前正在使用的物品,或者那些能从重要物品出发访问到的物品,并在它们上面贴上便利贴。
  • 清除(Sweep):然后走遍整个房间,把那些没有贴便利贴的东西统统清理出去。

分代垃圾回收

Generational Garbage Collection,中文叫做“分代垃圾回收”。

现代的 JavaScript 引擎(比如 Chrome 和 Node.js 中的 V8)通过“分代垃圾回收”机制对内存管理进行了优化。

对象会根据它们存在的时间被划分为不同的“代”:

  • 年轻代(Young generation):新创建的对象——其中大多数生命周期都很短。
  • 老年代(Old generation):经历多次垃圾回收后仍然存活的对象。

这就像管理你的邮箱:

  • 大多数邮件(临时对象)会很快被删除;
  • 重要的邮件(生命周期较长的数据)则会归档在文件夹中,很少再被频繁查看。

这种机制让垃圾回收更高效,因为 GC 不需要每次都扫描整个堆内存。

常见的内存泄漏

即使有自动垃圾回收机制,你仍然可能会造成内存泄漏——也就是某些数据在内存中停留的时间超过了它本该存在的时长。

意外的全局变量

如果你忘了使用关键字声明一个变量,JavaScript 会自动将其创建在全局作用域中——这样它就会一直存在,无法被回收。

function createLeak() {
  leakyVariable = "I'm global!"; // 缺少 'let'、'const' 或 'var'
}

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

定时器可能会无限期地保持对某些引用的引用。

let data = fetchLargeData()
setInterval(() => {
  doSomething(data);
}, 1000);

当定时器不再需要时,务必清除它们。

闭包持有引用

闭包可能会不小心“困住”某些变量,导致它们一直留在内存中。

function createClosure() {
  let largeArray = new Array(1000000);
  return function() {
    console.log("Hi!");
  };
}

尽管 largeArray 并未在返回的函数中使用,但它依然会保留在内存中,因为这个闭包仍然可以访问它。

这就像你一直带着一串钥匙,其中有些是早已不再使用的房子的钥匙——只要你不主动处理掉,这些钥匙(变量)就会一直存在。

脱离的 DOM 元素

如果你将一个元素从 DOM 中移除,但在 JavaScript 中仍然保留了对它的引用,那么它不会被垃圾回收。

let element = document.getElementById("myDiv");
document.body.removeChild(element);

在移除元素之后,应将对应的引用设为 null。

事件监听器

未移除的事件监听器可能会在元素已被移除后,依然将其保留在内存中。

element.addEventListener("click", handleClick);

当元素不再需要时,应及时移除对应的事件监听器。

监控内存使用情况

现代浏览器提供了内置工具,用于追踪内存使用:

  • Chrome DevTools → Memory 面板 — 可以拍摄堆快照并进行对比。
  • Performance Monitor — 查看实时内存使用情况。
  • 记录分配时间线 — 观察内存随时间的变化情况。

结语

JavaScript 具备自动内存管理功能,让你不必亲自操劳去手动释放内存,不过这并不意味着它毫无漏洞、绝对可靠。

垃圾回收机制仅能回收那些“确实无法再触及”的内存空间,对于那些你仍持有引用的数据,它是无能为力的。

这就好比家里雇了个管家,要是你一直不告知管家哪些屋子是闲置的,那管家自然也就不会去打扫那些地方。

以上关于JS中的内存管理是怎么工作的?的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

「点点赞赏,手留余香」

1

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » JS中的内存管理是怎么工作的?

发表回复