JavaScript垃圾回收是怎么工作的?

更新日期: 2025-12-01 阅读: 41 标签: 机制

作为JavaScript开发者,我们有一个其他语言程序员很少享受的"特权":几乎不用操心内存管理。我们创建变量、对象和函数,JavaScript引擎会自动处理内存分配和释放。这种"魔法"般的能力,靠的就是垃圾回收机制。

但垃圾回收到底是什么?它背后是怎么工作的?了解这个基础知识不仅能满足好奇心,还能帮你写出性能更好的代码,找出那些难查的内存问题。


什么是垃圾回收?

简单来说,垃圾回收是一种自动内存管理机制,专门回收程序中不再使用的对象占用的内存。当你在JavaScript中创建变量或对象时,引擎会为它们分配内存。随着时间的推移,有些内存可能不再需要了。如果没有垃圾回收,这些没释放的内存会越积越多,导致内存泄漏、性能变差,甚至程序崩溃。

JavaScript引擎(比如Chrome和Node.js用的V8、Firefox的SpiderMonkey、Safari的JavaScriptCore)会自动执行垃圾回收。和那些需要手动管理内存的语言不同,JavaScript开发者不能直接控制什么时候释放内存。


核心概念:可达性

垃圾回收的基本原则是可达性。如果一个对象能够从某些已知的"根"直接或间接访问到,它就被认为是"可达"的。

常见的根包括:

  • 全局对象:浏览器中的window、Node.js中的global

  • 当前调用栈:正在执行的函数中的局部变量和参数

  • 活跃的闭包:闭包中引用的变量

只要一个对象能通过引用链从根访问到,它就是"活着"的。无法访问的对象就是"不可达"的,也就是"垃圾",可以被回收。


JavaScript引擎如何做垃圾回收:标记-清除算法

虽然不同JavaScript引擎有各自的优化方法,但现代JavaScript最主要的垃圾回收算法是标记-清除。这个算法能处理实际应用中常见的复杂引用关系,包括循环引用。

标记-清除算法分为两步:

标记阶段

  • 从"根"开始(比如全局对象、调用栈)

  • 遍历整个对象图,沿着所有引用继续往下找

  • 所有能从根访问到的对象都会被标记为"正在使用"

你可以把这个过程想象成从起点开始的"寻宝":所有能找到的宝物都会被做上标记。

清除阶段

  • 标记完成后,垃圾回收器会扫描整个堆内存

  • 没被标记的对象被认为是不可达的

  • 这些对象占用的内存会被释放,留给后面使用

标记-清除算法的关键优势是能正确处理循环引用。比如A引用B、B引用A,但如果两者都不能从根访问到,就都会被当成垃圾回收。


引用计数的局限性

引用计数是一种更早、更简单的垃圾回收算法。每个对象记录自己被引用的次数,当引用数降到0时就被回收。

但引用计数处理不了循环引用。比如A引用B、B引用A时,即使它们不能从根访问,引用计数也不会变成0,导致内存泄漏。所以现代JavaScript引擎主要用标记-清除算法。


分代回收优化

现代引擎(比如V8)在标记-清除基础上加入了分代回收优化,这是基于"弱分代假说":

  • 大多数对象"死得很快"

  • 如果一个对象活了很长时间,它可能会继续活很久

引擎把堆内存分成几代,比如:

  • 新生代

  • 老生代

工作机制如下:

  • 新对象分配在新生代

  • 新生代比较小,垃圾回收更频繁但也更高效

  • 在新生代中存活多次的对象会"晋升"到老生代

  • 老生代垃圾回收次数少,但更彻底、花时间更长

这种方式能显著提升整体性能,避免频繁扫描整个堆内存。


JavaScript中常见的内存泄漏

即使有自动垃圾回收,下面这些情况还是会导致内存泄漏:

1. 意外的全局变量
如果忘记用let、const或var声明变量,它会变成全局变量,在程序关闭前都不会被回收。

function createLeakyVariable() {
  leakyVar = "我意外变成了全局变量!";
}
createLeakyVariable();

2. 忘记清理的定时器或回调
比如setInterval、setTimeout的回调,或者事件监听器中持有的引用,会阻止对象被回收。

let bigData = { /* 很大的对象 */ };
const button = document.getElementById('myButton');

button.addEventListener('click', function handler() {
  console.log(bigData);
});

3. 脱离dom的元素
从DOM中移除元素后,如果代码里还保留着引用,这个元素不会被回收。

4. 闭包问题
闭包可能无意中让外层作用域的变量存活时间过长。

5. 不合理的缓存策略
没有清理机制的缓存(比如放在全局对象里)会导致内存不断增长。


改善内存管理的实用方法

虽然垃圾回收是自动的,但开发者还是可以写出更容易被优化的代码:

A. 少用全局变量
多用let、const,避免变量变成全局的。

B. 及时移除事件监听器
当元素或组件不再需要时,记得移除事件监听器。

C. 清理定时器
用clearInterval、clearTimeout及时清理不需要的定时器。

D. 适时清空引用
对于生命周期长的对象或大型数据结构,适时把引用设为null可以明确释放。

E. 使用WeakMap和WeakSet
它们存储"弱引用",不会阻止对象被回收。

let user = { name: "张三" };
const userMetadata = new WeakMap();
userMetadata.set(user, { lastLogin: new Date() });

user = null;  // user对象现在可以被回收了

F. 使用性能分析工具
用浏览器开发者工具(比如Chrome的Performance和Memory)查看内存变化、生成堆快照,排查内存泄漏。


实际开发中的建议

1. 注意DOM引用

// 不好的做法
const elements = document.querySelectorAll('.item');
// 即使从DOM移除,elements仍然引用这些节点

// 好的做法
function processElements() {
  const elements = document.querySelectorAll('.item');
  // 处理元素...
  // 函数结束后,局部变量自动释放
}

2. 谨慎使用闭包

// 可能有问题
function createHeavyClosure() {
  const heavyData = new Array(1000000).fill('data');
  return function() {
    // 即使不用heavyData,它也会被保留
    console.log('hello');
  };
}

// 更好的做法
function createLightClosure() {
  return function() {
    console.log('hello');
  };
}

3. 及时清理资源

class DataManager {
  constructor() {
    this.timers = new Set();
    this.listeners = new Map();
  }
  
  addTimer(callback, delay) {
    const timer = setInterval(callback, delay);
    this.timers.add(timer);
    return timer;
  }
  
  cleanup() {
    // 清理所有定时器
    this.timers.forEach(timer => clearInterval(timer));
    this.timers.clear();
    
    // 清理所有事件监听器
    this.listeners.forEach((listener, element) => {
      element.removeEventListener('click', listener);
    });
    this.listeners.clear();
  }
}


总结

垃圾回收是JavaScript易用性的重要基础,让开发者不用手动处理复杂的内存管理。理解垃圾回收的核心原理——特别是标记-清除算法和可达性概念——有助于写出性能更好、更稳定的代码。通过避免常见问题和遵循最佳实践,可以让引擎更高效地管理内存,保持应用流畅运行,没有额外的内存负担。

记住,好的JavaScript代码不仅要功能正确,还要对内存友好。虽然垃圾回收是自动的,但我们的编码习惯会直接影响它的工作效率。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13243

浅析前端页面渲染机制

作为一个前端开发,最常见的运行环境应该是浏览器吧,为了更好的通过浏览器把优秀的产品带给用户,也为了更好的发展自己的前端职业之路,有必要了解从我们在浏览器地址栏输入网址到看到页面这期间浏览器是如何进行工作的

这一次,彻底弄懂 JavaScript 执行机制

javascript是一门单线程语言,Event Loop是javascript的执行机制.牢牢把握两个基本点,以认真学习javascript为中心,早日实现成为前端高手的伟大梦想!

创建js hook钩子_js中的钩子机制与实现

钩子机制也叫hook机制,或者你可以把它理解成一种匹配机制,就是我们在代码中设置一些钩子,然后程序执行时自动去匹配这些钩子;这样做的好处就是提高了程序的执行效率,减少了if else 的使用同事优化代码结构

小程序的更新机制_如何实现强制更新?

在讲小程序的更新机制之前,我们需要先了解小程序的2种启动模式,分别为:冷启动和热启动。小程序不同的启动方式,对应的更新情况不不一样的。无论冷启动,还是热启动。小程序都不会马上更新的,如果我们需要强制更新,需要如何实现呢?

基于JWT的Token认证机制实现及安全问题

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。其JWT的组成:一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

web前端-JavaScript的运行机制

本文介绍JavaScript运行机制,JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。

轮询机制解决后端任务回调问题

现在有一个需求,前端有一个按钮,点击以后会调用后端一个接口,这个接口会根据用户的筛选条件去hadoop上跑任务,将图片的base64转为img然后打包成zip,生成一个下载连接返回给前端,弹出下载框。hadoop上的这个任务耗时比较久

JavaScript预解释是一种毫无节操的机制

js代码执行之前,浏览器首先会默认的把所有带var和function的进行提前的声明或者定义:1.理解声明和定义、2.对于带var和function关键字的在预解释的时候操作不一样的、3.预解释只发生在当前的作用域下

js对代码解析机制

脚本执行js引擎都做了什么呢?1.语法分析 2.预编译 3.解释执行。在执行代码前,还有两个步骤;语法分析很简单,就是引擎检查你的代码有没有什么低级的语法错误 ,查找全局变量声明(包括隐式全局变量声明,省略var声明),变量名作全局对象的属性,值为undefined

web认证机制

以前对认证这方面的认识一直不太深刻,不清楚为什么需要token这种认证,为什么不简单使用session存储用户登录信息等。最近读了几篇大牛的博客才对认证机制方面有了进一步了解。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!