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

张开发
2026/4/17 14:09:25 15 分钟阅读

分享文章

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;barI am a leak;}避免方法始终使用 let、const 或 var 声明变量。开启 use strict 严格模式可以有效防止此类错误。B 被遗忘的定时器或回调如果循环定时器setInterval没有被显式清除它内部引用的变量将永远留在内存中。代码如下示例letsomeDataloadData();setInterval(function(){letnodedocument.getElementById(container);if(node){node.innerHTMLJSON.stringify(someData);}},1000);// 即使 container 节点被删除了定时器依然在运行someData 也就无法回收避免方法在不需要定时器时调用 clearInterval()。C 闭包Closures的不当使用闭包可以访问外部函数的变量。如果闭包长期存在它引用的变量也会被长期占用。letleakfunction(){letheavyDatanewArray(1000000);// 占用大内存returnfunction(){console.log(I still have access to heavyData);};};letstayInMemoryleak();// 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)这是现代浏览器最常用的算法。它分为两个阶段标记垃圾回收器从根对象开始遍历给所有能找到的对象打上“活跃”标记。清除遍历整个内存将那些没有标记的对象销毁回收内存空间。优点能够有效处理“循环引用”的问题即两个废弃对象互相引用但都与外界断开的情况。B. 引用计数算法 (Reference Counting)这是一种较老的策略现在基本不再单独使用。每当有一个地方引用该对象计数器 1 11。引用失效时计数器− 1 -1−1。当计数为 0 时内存立即被回收。缺点无法处理循环引用。如果 A 引用 BB 也引用 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-CompactMark-Sweep (标记-清除)遍历堆内存标记可达对象直接释放不可达对象的内存。Mark-Compact (标记-整理)为了解决清除后产生的碎片将所有存活对象推向内存的一端然后清理掉端边界以外的内存。区域存放对象特点主要算法新生代 (Young Gen)存活时间短的对象如局部变量Scavenge 算法将空间对半分为 From 和 To通过复制活跃对象来清理空间。老生代 (Old Gen)存活时间长或常驻的对象如全局变量标记-清除 (Mark-Sweep)与标记-整理 (Mark-Compact)。B 为什么必须分代如果不分代每次 GC 都进行全堆扫描会导致显著的性能问题降低 Stop-The-World (STW) 影响全堆扫描大型内存如 1.5G会使 JS 主线程暂停数百毫秒导致界面掉帧或卡死。分代后高频的新生代回收仅需几毫秒。效率优化“因材施教”。对短命对象用“空间换时间”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. 内存分配示意示例代码分析leta100;// a 是基础类型值 100 存在栈letb{name:Gemini};// b 的地址存在栈对象实体 {name: ...} 存在堆functionmultiply(x,y){// 函数实体存在堆letresultx*y;// 参数 x, y 和局部变量 result 存在该函数的栈帧中returnresult;}multiply(a,2);// 调用时创建栈帧结束时栈帧弹出result 销毁调用函数时 引擎会创建一个栈帧(Stack Frame)压入栈顶。存入参数和局部变量 x, y, c 被填入这个栈帧。函数返回后 该栈帧弹出内部的 x, y, c 瞬间被销毁。而堆内存中的 b 对象则等待垃圾回收器扫描。4. 为什么要这样划分栈高效。 栈遵循“先进后出”原则引擎只需要移动栈指针就能快速分配和释放内存适合频繁创建和销毁的局部变量。堆灵活。 引用类型的数据大小是不确定的比如数组可以动态推入元素栈无法预留空间因此放在堆中。通过在栈里存地址既保持了栈的轻量又实现了对大数据的访问。总结栈 存“快餐”基础类型、引用地址、执行逻辑。堆 存“大餐”复杂的对象、全局数据、闭包数据。

更多文章