【JS基础整理—No.01】 内存管理(垃圾回收、内存泄漏、弱引用)

本文介绍了JavaScript中的内存管理,包括内存分配、垃圾回收机制(引用计数法、标记清除法)以及如何尽量减少垃圾回收。此外,还详细讨论了内存泄漏的常见原因,如循环引用、闭包、全局变量和定时器,并引入了弱引用的概念,如WeakMap和WeakSet在防止内存泄漏中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 内存管理是指在声明一个变量时,我们为它分配一块内存空间,当不在需要时,将这块内存释放。在JS中开发者不需要手动进行内存管理,JS引擎会自动进行内存管理。

一、内存分配

当我声明变量时,我们要根据变量的数据类型进行内存分配。数据类型分为基本数据类型和引用数据类型。👇下面是一个简单的区别图解

基本类型:系统会为分配一块内存(栈),这块内存中保存的就是变量的内容。【String,Number,Boolean,Null,Undefined,Symbol】

引用类型:其存储的只是一个地址而已(栈),这个地址指向的内存块(堆)才是是变量的真正内容。【Object,Array,Function】

🪐:这里只简述一下数据类型在内存中的保存方式,其他关于数据类型的相关内容就不细说了。

二、内存释放(垃圾回收机制)

内存释放,即垃圾回收机制。当变量不再需要时,JS引擎把变量占用的内存回收。我们就需要知道如何判断变量不再需要。JavaScript 中有全局对象,浏览器中的全局对象是window,垃圾回收机制时从全局对象开始。

有一个官方解释叫可达性,我可太讨厌用概念来解释概念了。直接上图👇根据内存图,我们可以把可达性理解成“无法到达的岛屿🏝”

内存状态图解

情况一:

let user = {name: 'John'};  // 一个对象 {name:”John”}(称之为对象A)被创建,全局变量 “user” 引用了对象A
user = null;                // 如果user被覆盖,这个引用就没了
// 现在 对象A 变成不可达的了。因为没有引用了,就不能访问到它了。垃圾回收器会认为它是垃圾数据,然后释放内存。

情况二:

let user = {name: 'John'};  // 一个对象 {name:”John”}(称之为对象A)被创建,全局变量user引用了对象A
let admin = user;           // 全局变量admin引用了对象A
user = null;                // user被覆盖引用没了,但是对象A仍然可以通过 admin 这个全局变量访问到,所以对象还在内存中。
admin = null;               // 如果继续覆盖了 admin,对象就会被删除。

情况三: 

function marry(man, woman) {  
    woman.husband = man;  
    man.wife = woman;  
    return {    
        father: man,    
        mother: woman   
    }
}   
let family = marry( {name: "John"}, {name:"Ann"} ); 

// 现在移除两个引用
delete family.father;
delete family.mother.husband;

 

family = null;

两种方式

1.引用计数法:如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

let obj = { name: 'John' };    // 一个对象(称之为 A)被创建,赋值给 obj,A 的引用个数为 1 
let user = obj;             // A 的引用个数变为 2
obj = null;                 // A 的引用个数变为 1
console.log(user)           // 此时输出结果为{ a: 1 }
user = null;                // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了

局限性⚠:无法处理循环引用

function func() {
    let obj1 = {};
    let obj2 = {};
 
    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

2.标记清除法

垃圾回收机制时从全局对象开始,【开始念经】找所有从这个全局对象开始引用的对象,再找这些对象引用的对象......对这些活着的对象进行标记,这是标记阶段。【有点绕】清除阶段就是清除那些没有被标记的对象。垃圾回收的这个基本算法被称为 “mark-and-sweep”,从根开始遍历标记,没有被遍历过的对象则被清除。

 这个过程自动完成,为了不影响代码执行,JS引擎做出的许多优化:

  • 分代收集 —— 对象被分成两组:『新的』和『旧的』。许多对象出现,完成他们的工作并快速释放,他们可以很快被清理。那些长期存活下来的对象会变得『老旧』,而且检查的次数也会减少。

  • 增量收集 —— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分逐一处理。这需要他们之间额外的标记来追踪变化,但是会有许多微小的延迟而不是大的延迟。

  • 闲时收集 —— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

尽量减少垃圾回收

浏览器可以进行垃圾自动回收,但当代码比较复杂时,垃圾回收代价太大。

  1. 数组:最简单的办法赋值[],但同时创建了一个空对象;可以设置数组长度为0
  2. 对象:赋值null
  3. 函数:在循环的函数表达式中,如果可以复用,尽量放在函数的外面

三、内存泄漏

内存泄漏是指计算机可用的内存越来越少,主要是由于程序不能释放那些不再使用的内存。

常见的几种情况:

  1. 上面👆提到的循环引用

  2. 闭包使局部变量常驻内存,需要手动清除

  3. 无意声明的全局变量

    function foo(arg) {
        const bar = "";
    } 
    foo();
    // 当 foo 函数执行后,变量 bar 就会被标记为可回收。
    // 因为当函数执行时,函数创造了一个作用域来让函数里的变量在里面声明。
    // 进入这个作用域后,浏览器就会为变量 bar 创建一个内存空间。
    // 当这个函数结束后,其所创建的作用域里的变量也会被标记为垃圾,在下一个垃圾回收周期到来时,这些变量将会被回收。
    // 改👇
    function foo(arg) {
        bar = "";
    }
    foo();
    // 无意中声明了一个全局变量,会得到 window 的引用,bar 实际上是 window.bar,
    // 它的作用域在 window 上,所以 foo 函数执行结束后,bar 也不会被内存收回。
    ​
    // 另一种情况
    function foo() {
        this.bar = "";
    }
    foo();
    // 函数自调用this 指的是 window,犯的错误跟上面类似,创建了一个全局对象。
  4. 被遗忘的定时器或回调函数(在别的博客中看到的,还没有遇到过实际场景)

    设置了setInterval定时器,但忘记取消,如果循环函数有对外部变量的引用的话,这个变量就会一直存在内存。
  5. 脱离DOM的引用

    获得一个DOM元素的引用,后面这个元素被删除。由于一直保存了这个元素的引用,所以无法被回收。

四、补充:弱引用

在WeakSet和Weak Map中,我第一次了解到弱引用,然后我想着捋一下垃圾回收和内存泄漏这部分的内容,之后就有了这篇文章。

前面我们所提到的引用都是强引用,弱引用一个很特别的存在,就是垃圾回收机制不考虑该对象的引用。

解释一下就是,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象是否还在该弱引用的结构中。

基于弱引用的数据结构:WeakMap、WeakSet、WeakRef。Weak Set中的对象引用是弱引用。WeakMap中的键也是弱引用(值不是)。

🌰 以WeakMap解释一下弱引用

有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1e2占用的内存。一旦忘记手动删除,就会造成内存泄露。请看下面的例子🌰。

const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;

WeakMap的设计目的在于,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。举个例子🌰

const wm = new WeakMap();
const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

🚗 参考资料

JS 中的垃圾回收

V8 内存分配与垃圾回收

ES6 入门教程-weakMap​​​​​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值