JavaScript垃圾回收机制实现方法
作者:超级无敌大蟑王
1.垃圾回收的概念
JavaScript使用垃圾自动回收机制进行内存管理,无需程序员手动分配和释放内存。垃圾回收的基本思路是确定哪些变量不会再次被使用,然后回收这些变量占用的内存。垃圾回收过程是周期性的,垃圾回收程序每隔一段时间会运行一次,垃圾回收也会影响到应用程序的性能。常用的垃圾回收机制主要包括标记清除和引用计数。
1.1 什么是垃圾回收机制:
GC
即 Garbage Collection
,程序工作过程中会产生很多"垃圾",这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而 GC
就是负责回收垃圾的,因为他工作在引擎内部,所以对于我们前端来说,GC
过程是相对比较无感的,这一套引擎执行而对我们又相对无感的操作也就是常说的 垃圾回收机制
不是所有语言都有 GC
,一般的高级语言里面会自带 GC
,比如 Java、Python、JavaScript
等,也有无 GC
的语言,比如 C、C++
等,那这种就需要我们程序员手动管理内存了,相对比较麻烦
在像 C/C++ 这样的语言中,开发者需要手动分配(malloc
)和释放(free
)内存。这种方式非常灵活,性能也高,但有两个致命缺点:
- 忘记释放:会导致内存泄漏,程序占用的内存会随着时间推移越来越多,最终可能导致程序崩溃或系统变慢。
- 提前释放:或释放了多次,会导致悬挂指针,当程序尝试访问一个已经被释放的内存地址时,会引发不可预知的错误(通常是程序崩溃)。
JavaScript中存在两种变量:局部变量
和全局变量
。全局变量的生命周期会持续到页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。(不过,当局部变量被外部函数使用时,其中一种情况就是闭包
,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。)
2.垃圾回收机制是如何实现的
2.1核心理念:
GC 机制的核心思想是可达性
。简单来说,就是判断一个对象是否“可达”,如果不可达,它就是“垃圾”。
- 可达的对象:以某种方式被访问或使用的对象。
- 不可达的对象:无法被访问到的对象,可以被安全地回收。
GC 会有一系列的根对象,它们是可达性的起点。在 JavaScript 中,主要的根包括:
全局对象
:比如浏览器环境下的 window
对象,Node.js 环境下的 global
对象。
因为全局对象在应用程序的整个生命周期内都存在。只要你的网页开着,window
对象就永远不会消失。它是所有全局变量和内置API(如 setTimeout
, localStorage
)的宿主。如果它被回收了,整个JavaScript环境就都将崩塌。因此,全局对象是GC最重要、最基础的一个“根”。
函数调用栈
:当前正在执行的函数中的局部变量和参数。
因为当前正在执行的代码和它所依赖的数据,理所当然是“存活”的。调用栈代表了程序执行的“此时此刻”。如果这些正在使用的变量被回收了,程序将立即出错。因此,调用栈中所有栈帧里的变量和参数,都被视为临时的“根”。
活跃的 DOM 树
:页面上存在的 DOM 元素。
因为DOM节点是构成用户界面的实体。用户能看到、能与之交互的元素,必须始终存在于内存中,浏览器需要依据它来进行绘制和响应事件。因此,所有在DOM树上的节点都被认为是“可达的”。
垃圾回收器会从这些“根”
出发,沿着引用链进行遍历。所有能从“根”访问到的对象,都会被认为是“活”的(可达的);反之,所有无法从“根”访问到的对象,就会被认为是“死”的(不可达的),并成为垃圾回收的目标。
// 创建一个根对象,并被变量 user 引用 let user = { name: "Alice" }; // 原来的 { name: "Alice" } 对象失去了引用,因此它变成了不可达对象,等待被回收。 user = null;
2.2主流垃圾回收算法
浏览器通常使用的垃圾回收方法有两种:标记清除
,引用计数
。
2.2.1标记清除
这是现代浏览器中最常用的垃圾回收算法。它完美地解决了循环引用
的问题。
- 原理:分为两个阶段:
- 标记阶段:垃圾回收器从
“根”对象
开始,遍历所有可达的对象,并在这些对象上打上一个“标记”,表示它们是存活的。 - 清除阶段:垃圾回收器遍历整个
堆内存
,所有没有被标记的对象都被视为垃圾,并被回收,其占用的内存被释放。
- 标记阶段:垃圾回收器从
- 优点:可以解决循环引用的问题。因为即使
objA
和objB
互相引用,但如果它们都无法从“根”访问到,那么它们就都不会被标记,最终会被一起清除。
function createCircularReference() { let objA = {}; let objB = {}; objA.b = objB; objB.a = objA; } createCircularReference();
- 缺点:
- 执行效率问题:GC 执行时,需要
暂停整个程序
的运行,如果堆内存很大,标记和清除会很耗时。 - 内存碎片化:清除后,会产生大量不连续的内存碎片。如果之后需要分配一个大对象,可能会因为没有足够大的
连续空间
而失败。
- 执行效率问题:GC 执行时,需要
2.2.2引用计数
这是早期的一种 GC 算法,思想非常简单。
- 原理:为每个对象维护一个
“引用计数器”
。当有一个引用指向该对象时,计数器加1;当引用被移除时,计数器减1。当计数器变为0时,表示该对象不再被需要,可以被回收。 - 优点:实现简单,垃圾可以被
立即回收
,不会有“暂停”的感觉。 - 致命缺点:无法处理
循环引用
。
看下面的例子:
这种情况下,就要手动释放变量占用的内存:
obj1.a = null obj2.a = null
2.3 V8 引擎的优化:分代回收
为了解决标记-清除算法的效率问题,Google 的 V8 引擎(用于 Chrome 和 Node.js)采用了一种更先进的策略:分代回收
。
这个策略基于一个重要的观察:“大部分对象都是朝生夕死的”。也就是说,很多对象在创建后很快就不再被使用,而少数对象会存活很长时间。
V8 将堆内存分为两个主要区域:
新生代:Scavenge
算法
- 特点:存放生命周期短的对象,空间较小(通常为 1-8MB),垃圾回收频繁且
速度快
。 - 内部结构:新生代内存被平分为两个相等的空间:
From 空间(使用中)
和To 空间(空闲)
。 - **回收过程:
- 新对象首先被分配在 From 空间。
- 当 From 空间快要被占满时,触发一次新生代的 GC。
- GC 会检查 From 空间中的存活对象,并将它们复制到 To 空间。
- 复制完成后,From 空间剩下的所有对象都是垃圾。整个 From 空间被一次性清空。
- From 空间和 To 空间的角色互换,等待下一次 GC。
- 晋升:如果一个对象在新生代中经过了多次 Scavenge 依然存活,那么它被认为是生命周期较长的对象,会被“晋升”到老生代中。此外,如果复制一个对象到 To 空间时,To 空间的使用率超过了25%,该对象也会被直接晋升到老生代。
老生代:标记-清除与 标记-整理 - 特点:存放生命周期长或体积大的对象,空间较大,GC
频率较低
。 - 回收过程:
- 主要使用标记-清除算法,流程如前所述。
- 为了解决内存碎片化问题,V8 引入了标记-整理算法。它在标记阶段之后,不是直接清除垃圾,而是将所有存活的对象向内存的一端移动,然后直接清理掉边界之外的所有内存。这样就得到了连续的空闲空间。
3.减少垃圾回收
3.1 手动处理:
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对
数组
进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。 - 对
对象
进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
避免意外的全局变量:
始终使用 const
或 let
声明变量,开启严格模式('use strict';
)。
function leakyFunction() { // 如果没有 'let' 或 'const', a 会被创建为全局变量 // 它将永远不会被回收,除非手动设为 null a = new BigObject(); }
警惕闭包:闭包
是 JavaScript 的强大特性,但也很容易造成内存泄漏。闭包可以使其父函数中的变量在函数执行结束后仍然存活。
function createClosure() { let largeData = new Array(1e6).fill('*'); // 这个返回的函数持有了对 largeData 的引用 return function() { ... return largeData.length; }; } let myClosure = createClosure(); // 即使 createClosure 执行完毕,largeData 也不会被回收,因为它被 myClosure 引用。 // 如果不再需要它,应手动解除引用。 myClosure = null;
定时器和事件监听器:setInterval
, setTimeout
和 addEventListener
如果不被正确清理,它们的回调函数和其引用的外部变量都不会被回收。
let element = document.getElementById('my-button'); let largeData = new BigObject(); function onClick() { // do something with largeData } element.addEventListener('click', onClick); // 正确做法: // element.removeEventListener('click', onClick); // element = null;
在组件销毁或元素移除时,务必使用 clearInterval
, clearTimeout
和 removeEventListener
清理掉相关的定时器和监听器。
面试标准答案(背下来!)
Q:请简述 JavaScript 的垃圾回收机制。A:
JavaScript 的垃圾回收(GC)是自动内存管理机制,核心是标记清除和引用计数。
- 标记清除:从根对象(如全局变量)出发,标记所有可达对象,清除未标记的内存。优点是解决循环引用问题,缺点是执行时可能短暂卡顿。
- 引用计数:记录每个对象的引用次数,归零时立即回收。缺点是无法处理循环引用,可能导致内存泄漏。
V8 引擎的优化:
- 分代回收:新生代(存活时间短)用Scavenge算法,老生代用标记清除/整理。
- 增量回收:将GC任务拆分成小任务,减少卡顿。。
避免内存泄漏:
- 避免意外全局变量。
- 及时清除定时器、事件监听。
- 谨慎使用闭包引用大对象。
- 清理无用的 DOM 引用。
到此这篇关于JavaScript垃圾回收机制的文章就介绍到这了,更多相关js垃圾回收机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!