JavaScript垃圾回收机制

1.垃圾回收的概念

1.1 什么是垃圾回收机制:

GCGarbage 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标记清除

这是现代浏览器中最常用的垃圾回收算法。它完美地解决了循环引用的问题。

  • 原理:分为两个阶段:
    1. 标记阶段:垃圾回收器从“根”对象开始,遍历所有可达的对象,并在这些对象上打上一个“标记”,表示它们是存活的。
    2. 清除阶段:垃圾回收器遍历整个堆内存,所有没有被标记的对象都被视为垃圾,并被回收,其占用的内存被释放。
  • 优点:可以解决循环引用的问题。因为即使 objAobjB 互相引用,但如果它们都无法从“根”访问到,那么它们就都不会被标记,最终会被一起清除。
function createCircularReference() {
  let objA = {};
  let objB = {};

  objA.b = objB; 
  objB.a = objA; 
}

createCircularReference();
  • 缺点
    • 执行效率问题: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 空间(空闲)
  • **回收过程:
    1. 新对象首先被分配在 From 空间。
    2. 当 From 空间快要被占满时,触发一次新生代的 GC。
    3. GC 会检查 From 空间中的存活对象,并将它们复制到 To 空间。
    4. 复制完成后,From 空间剩下的所有对象都是垃圾。整个 From 空间被一次性清空。
    5. From 空间和 To 空间的角色互换,等待下一次 GC。
  • 晋升:如果一个对象在新生代中经过了多次 Scavenge 依然存活,那么它被认为是生命周期较长的对象,会被“晋升”到老生代中。此外,如果复制一个对象到 To 空间时,To 空间的使用率超过了25%,该对象也会被直接晋升到老生代。
    老生代:标记-清除与 标记-整理
  • 特点:存放生命周期长或体积大的对象,空间较大,GC 频率较低
  • 回收过程
    1. 主要使用标记-清除算法,流程如前所述。
    2. 为了解决内存碎片化问题,V8 引入了标记-整理算法。它在标记阶段之后,不是直接清除垃圾,而是将所有存活的对象向内存的一端移动,然后直接清理掉边界之外的所有内存。这样就得到了连续的空闲空间。

3.减少垃圾回收

3.1 手动处理:
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。

  • 数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
  • 对象进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。

避免意外的全局变量
始终使用 constlet 声明变量,开启严格模式('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, setTimeoutaddEventListener 如果不被正确清理,它们的回调函数和其引用的外部变量都不会被回收。

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, clearTimeoutremoveEventListener 清理掉相关的定时器和监听器。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值