当前位置: 首页 > news >正文

js内存泄露与垃圾回收机制

文章目录

  • 一、内存泄漏
    • 1. 什么是内存泄露
    • 2. 常见内存泄漏场景及代码示例
      • A 意外的全局变量
      • B 被遗忘的定时器或回调
      • C 闭包(Closures)的不当使用
      • D 脱离 DOM 的引用
  • 二、JavaScript 垃圾回收机制 (Garbage Collection)
      • 1. 垃圾回收的核心判断标准:可达性 (Reachable)
      • 2. 核心算法
        • A. 标记-清除算法 (Mark-and-Sweep)
        • B. 引用计数算法 (Reference Counting)
      • 3. V8 引擎的优化:分代回收 (Generational Collection)
        • A 核心内存分代架构
        • B 为什么必须分代?
        • C V8 进阶优化技术
      • 4. 常见的内存泄漏场景
  • 三、JavaScript 运行时内存布局解析
    • 1. 内存空间划分概览
    • 2. 不同 JS 内容的存放位置
      • 2.1 基础类型变量 (Primitive Types)
      • 2.2 引用类型对象 (Reference Types)
      • 2.3 函数 (Functions)
      • 2.4 局部变量 (Local Variables)
      • 2.5 全局变量 (Global Variables)
      • 2.6 闭包中的变量 (Variables in Closures)
    • 3. 内存分配示意
    • 4. 为什么要这样划分?

一、内存泄漏

1. 什么是内存泄露

简单来说,内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃。

在 JavaScript 中,这通常意味着一些本该被垃圾回收机制(GC)销毁的对象,由于某些非预期的引用链,依然被标记为“可达”。

2. 常见内存泄漏场景及代码示例

A 意外的全局变量

全局变量的生命周期与页面一致,除非页面关闭,否则不会被回收。

functionleak(){// 这种写法等同于 window.bar = "I am a leak";bar="I am a leak";}

避免方法:始终使用 let、const 或 var 声明变量。开启 use strict 严格模式可以有效防止此类错误。

B 被遗忘的定时器或回调

如果循环定时器(setInterval)没有被显式清除,它内部引用的变量将永远留在内存中。
代码如下(示例):

letsomeData=loadData();setInterval(function(){letnode=document.getElementById('container');if(node){node.innerHTML=JSON.stringify(someData);}},1000);// 即使 container 节点被删除了,定时器依然在运行,someData 也就无法回收

避免方法:在不需要定时器时,调用 clearInterval()。

C 闭包(Closures)的不当使用

闭包可以访问外部函数的变量。如果闭包长期存在,它引用的变量也会被长期占用。

letleak=function(){letheavyData=newArray(1000000);// 占用大内存returnfunction(){console.log("I still have access to heavyData");};};letstayInMemory=leak();// heavyData 无法被回收

避免方法:只有在确有必要时使用闭包,或者在闭包使用完后将持有闭包的变量置为 null。

D 脱离 DOM 的引用

有时候你在 JS 里保存了对 DOM 节点的引用,但之后这个节点在页面上被删除了。由于 JS 变量依然指向它,该 DOM 节点及其子元素都无法被回收。

letelements={button:document.getElementById('button')};functionremoveButton(){document.body.removeChild(document.getElementById('button'));// 此时虽然页面上没按钮了,但 elements.button 依然引用着它}

避免方法:删除 DOM 节点后,手动将对应的变量设为 null。

二、JavaScript 垃圾回收机制 (Garbage Collection)

简单来说,JavaScript 的垃圾回收机制(简称 GC)就像是一个自动化的“内存清洁工”。

在编写代码时,我们会创建变量、对象和函数,这些都会占用计算机的内存。如果这些不再使用的内存不被释放,就会导致“内存泄漏”,最终让程序变卡甚至崩溃。JS 引擎(如 Chrome 的 V8)会自动找出那些不再需要的变量,并释放它们占用的空间。


1. 垃圾回收的核心判断标准:可达性 (Reachable)

JS 判断一个对象是否需要被回收,主要看它是否“可达”

  • 根对象 (Roots):包括全局对象 (window/global)、当前执行栈中的局部变量和参数等。
  • 可达:如果从根对象出发,通过引用链条能够找到某个对象,说明该对象还在使用,不能回收。
  • 不可达:如果一个对象没有任何引用指向它,或者从根部出发无法访问到它,它就被视为“垃圾”。

2. 核心算法

A. 标记-清除算法 (Mark-and-Sweep)

这是现代浏览器最常用的算法。它分为两个阶段:

  1. 标记:垃圾回收器从根对象开始遍历,给所有能找到的对象打上“活跃”标记。
  2. 清除:遍历整个内存,将那些没有标记的对象销毁,回收内存空间。

优点:能够有效处理“循环引用”的问题(即两个废弃对象互相引用,但都与外界断开的情况)。

B. 引用计数算法 (Reference Counting)

这是一种较老的策略,现在基本不再单独使用。

  • 每当有一个地方引用该对象,计数器+ 1 +1+1
  • 引用失效时,计数器− 1 -11
  • 当计数为 0 时,内存立即被回收。

缺点:无法处理循环引用。如果 A 引用 B,B 也引用 A,它们的计数永远不为 0,导致内存泄漏。


3. V8 引擎的优化:分代回收 (Generational Collection)

JavaScript 的内存管理是自动化的,V8 引擎通过高效的垃圾回收策略来平衡程序性能与内存占用。

V8 引擎采用分代回收(Generational Collection)的核心理由是基于计算机科学中一个著名的假说:“弱分代假说”(The Generational Hypothesis)。

  • 核心观点: 绝大多数对象在内存中存活的时间都很短(“朝生夕死”),而熬过多次回收的对象通常会存活很久。

为了兼顾 **“回收频率” **和 **“扫描性能” **,V8 将堆内存划分为两个主要的区域:新生代(Young Generation)和老生代(Old Generation)。


A 核心内存分代架构

V8 将堆内存分为新生代 (Young Generation)老生代 (Old Generation),并对它们采用不同的回收策略。

新生代 (Young Generation)

  • 适用对象:存活时间短的对象(如局部变量、临时计算结果)。
  • 内存特征:空间小(通常 1MB - 8MB),回收极其频繁。
  • 核心算法:Scavenge (Semi-space)
    • 划分:内存被对半分为From-space(活跃区)和To-space(空闲区)。
    • 分配:新对象始终在From-space中分配。
    • 标记与复制:当From-space快满时,GC 开始运行。它将From-space中的存活对象连续复制到To-space
    • 整理:复制过程中,对象被紧凑地排列,直接解决了内存碎片问题。
    • 角色翻转:清空From-space,然后交换两个空间的角色。
  • 晋升机制:对象如果经历过两次 Scavenge 回收依然存活,或者To-space占比超过 25%,则会被移动到老生代。

老生代 (Old Generation)

  • 适用对象:存活时间长(如全局变量、闭包)或体积巨大的对象。
  • 核心算法:Mark-Sweep & Mark-Compact
    • Mark-Sweep (标记-清除):遍历堆内存,标记可达对象,直接释放不可达对象的内存。
    • Mark-Compact (标记-整理):为了解决清除后产生的碎片,将所有存活对象推向内存的一端,然后清理掉端边界以外的内存。
区域存放对象特点主要算法
新生代 (Young Gen)存活时间短的对象(如局部变量)Scavenge 算法:将空间对半分为 From 和 To,通过复制活跃对象来清理空间。
老生代 (Old Gen)存活时间长或常驻的对象(如全局变量)标记-清除 (Mark-Sweep)标记-整理 (Mark-Compact)

B 为什么必须分代?

如果不分代,每次 GC 都进行全堆扫描,会导致显著的性能问题:

  1. 降低 Stop-The-World (STW) 影响:全堆扫描大型内存(如 1.5G)会使 JS 主线程暂停数百毫秒,导致界面掉帧或卡死。分代后,高频的新生代回收仅需几毫秒。
  2. 效率优化:“因材施教”。对短命对象用“空间换时间”(Scavenge 复制快),对长寿对象用“时间换空间”(Mark-Compact 减少浪费)。

C V8 进阶优化技术

为了将 STW 的卡顿感降到最低,现代 V8 引入了多种并行与异步技术:

技术名称原理说明
增量标记 (Incremental Marking)将一次完整的标记分解成多个小步,每执行一会儿就让出主线程给 JS 代码,交替进行。
并发回收 (Concurrent GC)在主线程执行 JS 的同时,辅助线程在后台进行标记和清理工作。
并行回收 (Parallel GC)主线程和辅助线程同时执行 GC 工作,利用多核 CPU 加速回收过程。
延迟清理 (Lazy Sweeping)在标记完成后,不立即清理所有垃圾,而是根据内存需求按需清理。

4. 常见的内存泄漏场景

虽然有自动回收,但以下不当操作会导致内存无法释放:

泄露原因典型代码示例预防措施
意外的全局变量function f() { leak = "data"; }开启"use strict",始终使用const/let
未清理的定时器setInterval(() => { ... }, 1000)页面卸载前调用clearInterval()
闭包引用未释放闭包持有大对象且长期被引用及时将外部持有闭包的变量置为null
脱离 DOM 的引用let elements = { btn: document.getElementById('id') }DOM 删除后,同步手动清除 JS 中的引用。

总结:垃圾回收是自动的,但理解其“可达性”原则能帮我们写出更高效、更健壮的代码。

三、JavaScript 运行时内存布局解析

在 JavaScript 引擎(如 V8)执行代码时,内存空间主要根据数据的生命周期和大小,划分为栈 (Stack)堆 (Heap)两大部分。


1. 内存空间划分概览

区域存储内容管理方式特点
栈内存 (Stack)基础类型数据、引用地址、执行上下文系统自动分配与释放(后进先出)空间小、访问速度极快
堆内存 (Heap)引用类型对象(对象、数组、函数)垃圾回收机制 (GC) 自动管理空间大、存储灵活
代码区 (Code)编译后的机器码/字节码只读存储程序逻辑

2. 不同 JS 内容的存放位置

2.1 基础类型变量 (Primitive Types)

  • 包含Number,String,Boolean,null,undefined,Symbol,BigInt
  • 位置栈内存
  • 原因:这些数据占用空间固定,大小已知,存储在栈中可以实现快速读取。

2.2 引用类型对象 (Reference Types)

  • 包含Object,Array,Function
  • 位置堆内存
  • 注意:虽然对象实体存放在堆中,但在栈内存中会保留一个指向该堆地址的指针(引用地址)。

2.3 函数 (Functions)

  • 函数名:存放在栈内存(或闭包相关的空间)中,作为变量指向堆地址。
  • 函数体:作为对象存放在堆内存中。
  • 函数参数:存放在栈内存中(当前执行上下文的栈帧里),随函数执行完毕而销毁。

2.4 局部变量 (Local Variables)

  • 位置栈内存
  • 生命周期:随函数调用而创建,随函数执行结束而弹出栈并销毁。

2.5 全局变量 (Global Variables)

  • 位置堆内存(老生代)。
  • 原因:全局变量生命周期极长(直到页面关闭),因此被存放在老生代区域中,由 GC 维护。

2.6 闭包中的变量 (Variables in Closures)

  • 位置堆内存
  • 原因:由于闭包允许函数访问外部作用域,即使外部函数执行完毕,变量也不能被销毁。为了长久保存,引擎会将这些变量从栈“逃逸”到堆内存中。

3. 内存分配示意

示例代码分析:

leta=100;// a 是基础类型,值 100 存在栈letb={name:"Gemini"};// b 的地址存在栈,对象实体 {name: "..."} 存在堆functionmultiply(x,y){// 函数实体存在堆letresult=x*y;// 参数 x, y 和局部变量 result 存在该函数的栈帧中returnresult;}multiply(a,2);// 调用时创建栈帧,结束时栈帧弹出,result 销毁
  1. 调用函数时: 引擎会创建一个栈帧(Stack Frame),压入栈顶。

  2. 存入参数和局部变量: x, y, c 被填入这个栈帧。

  3. 函数返回后: 该栈帧弹出,内部的 x, y, c 瞬间被销毁。而堆内存中的 b 对象则等待垃圾回收器扫描。

4. 为什么要这样划分?

栈:高效。 栈遵循“先进后出”原则,引擎只需要移动栈指针就能快速分配和释放内存,适合频繁创建和销毁的局部变量。

堆:灵活。 引用类型的数据大小是不确定的(比如数组可以动态推入元素),栈无法预留空间,因此放在堆中。通过在栈里存地址,既保持了栈的轻量,又实现了对大数据的访问。

总结:

  • 栈 = 存“快餐”(基础类型、引用地址、执行逻辑)。

  • 堆 = 存“大餐”(复杂的对象、全局数据、闭包数据)。

http://www.cnnetsun.cn/news/1944102.html

相关文章:

  • 别再死记公式了!用Multisim 14.2仿真带你直观理解RLC并联谐振(附仿真文件)
  • VUE实战:amfe-flexible + postcss-pxtorem 打造跨设备移动端自适应布局
  • 3个步骤搞定Windows PDF处理难题:Poppler零依赖解决方案
  • AI生成算法的“可信度衰减曲线”首次建模(基于SITS2026 17家头部厂商实测数据),第8轮迭代后准确率断崖式下跌!
  • VMware17黑苹果虚拟机搭建全攻略:从零开始运行macOS13
  • 终极Windows运行时库管理方案:VisualCppRedist AIO完整指南
  • Java项目集成Tesseract OCR:从环境搭建到跨平台部署实战
  • 别再手动改配置了!用Docker Compose一键部署Pikachu靶场,5分钟搞定渗透测试环境
  • 手眼标定界面不显示?别慌,一个pip install PySide2搞定easy_handeye的rqt窗口问题
  • 从CubeMX工程到ROS2节点:一份给STM32开发者的MicroROS集成实战指南
  • Windhawk:像安装App一样轻松定制Windows,告别复杂的系统修改
  • D2RML终极指南:暗黑破坏神2重制版多开工具完全解析
  • 别再手动编译了!CentOS 7下一键脚本搞定Tinyproxy 1.11.1带密码验证的代理服务器
  • 从搜索引擎到ChatBot:深入浅出聊聊余弦相似度在AI产品里的那些‘隐藏’用法
  • SBT 1.4.4 镜像配置踩坑实录:从 `repositories` 文件格式到 `overrideBuildResolvers` 参数详解
  • Ansys Maxwell实战:电机磁芯损耗建模全流程(附避坑指南)
  • 【企业级AI应用落地生死线】:SITS2026定义的3个不可妥协SLA指标及实时监控方案
  • 地平线DiffusionDrive v1和v2的细节对比解读
  • 7-Zip完整指南:如何用这款免费开源压缩工具提升工作效率 [特殊字符]
  • 暗黑破坏神2存档编辑器:现代化Web界面下的角色数据管理方案
  • 口袋里的AI超算?手把手教你用Project DIGITS本地部署和微调Llama Nemotron模型
  • 5分钟快速上手:用JavaScript自动化生成专业PPT的终极指南
  • 国家中小学智慧教育平台电子课本解析工具:一键下载PDF教材的终极解决方案
  • Redis Cluster 故障恢复与节点迁移
  • 用NetLogo模拟疫情传播:从零搭建小世界与无标度网络SEIR模型(附完整源码)
  • 微信小程序web-view集成H5视频录制:从需求到填坑的完整实践
  • Ubuntu-Rockchip实战:Rockchip RK35XX设备无线驱动深度集成与最佳实践
  • MySQL 中 VARCHAR、TEXT 与 JSON 类型:区别、场景与选型指南
  • FineBI vs Tableau功能对比:可视化能力与易用性全面评测
  • 【2026奇点大会AI语音克隆终极指南】:3大伦理红线、5类商用陷阱与7步合规部署清单