【学习笔记】内存生命周期、垃圾回收机制、内存泄漏

JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。不再用到的内存,没有及时释放, 就叫做内存泄漏。

内存生命周期

不管什么程序语言,内存生命周期基本是一致的, 一般有如下生命周期:

  1. 内存分配 :分配所使用的内存
  2. 内存使用 :使用分配到的内存(即读、写内存)
  3. 内存回收 :使用完毕,由垃圾回收自动回收不再使用的内存

说明:全局变量一般不会回收(关闭页面回收); 一般情况下局部变量的值不用了, 会被自动回收

内存分配

JavaScript 是在在定义变量时就完成了内存分配,也就是不需要我们手动进行分配。

var number = 123; // 给数值变量分配内存
var srting = "abc"; // 给字符串分配内存

var object = {
  a: 1,
  b: null,
}; // 给对象及其包含的值分配内存

// 给数组及其包含的值分配内存(就像对象一样)
var arr = [1, null, "abra"];

function fn(a) {
  return a + 2;
} // 给函数(可调用的对象)分配内存

// 函数表达式也能分配一个对象
someElement.addEventListener(
  "click",
  function () {
    someElement.style.backgroundColor = "blue";
  },
  false,
);

//有些函数调用结果是分配对象内存
var d = new Date(); // 分配一个 Date 对象

内存使用

使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。

// 读取内存
console.log(number); // 输出 123

// 修改内存
object.a = 2; // 修改 age 属性
console.log(object.a); // 输出 2

// 使用数组
arr.push(6); // 向数组添加新元素
console.log(arr); // 输出 [1, null, "abra", 6]

内存回收

当变量不再被使用时,JavaScript 的垃圾回收机制会自动回收不再使用的内存。栈内存中的基本数据类型,可以直接通过操作系统进行处理,而堆内存中的引用数据类型的值大小不确定,因此需要JS的引擎通过垃圾回收机制进行处理。

垃圾回收机制

两种常见的浏览器 垃圾回收算法 : 引用计数法 和 标记清除法

引用计数法

IE采用的引用计数算法, 定义“ 内存不再使用 ”。这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

简单来说就是:

  1. 记录引用的次数,被引用 n 次,就记录次数 n
  2. 如果减少一个引用就减 n–
  3. 如果引用次数是 0 ,则释放内存
var o = {
  a: {
    b: 2,
  },
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量 o
// 很显然,没有一个可以被垃圾收集

var o2 = o; // o2 变量是第二个对“这个对象”的引用

o = 1; // 现在,“这个对象”只有一个 o2 变量的引用了,“这个对象”的原始引用 o 已经没有

var oa = o2.a; // 引用“这个对象”的 a 属性
// 现在,“这个对象”有两个引用了,一个是 o2,一个是 oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性 a 的对象还在被 oa 引用,所以还不能回收

oa = null; // a 属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
限制:循环引用

如果两个对象 相互引用 ,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。

function f() {
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o
  return "引用计数算法无法回收,内存泄漏";
}
f();
标记清除法

从 2012 年起,所有现代浏览器都使用了标记清除垃圾回收算法。

简单来说就是:

  1. 标记清除算法将“不再使用的对象”定义为“ 无法达到的对象 ”。
  2. 垃圾回收器将定期从一个叫做根(root)的对象(在JavaScript 里,根是全局对象)开始,找所有从根开始引用的对象,然后找这些对象引用的对象……
  3. 从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
  4. 所有不能获得的对象,将会被回收,释放内存

解决了循环引用。在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。

内存泄漏

内存泄漏是指程序中不再使用的内存没有被及时回收,从而导致可用内存逐渐减少,最终可能导致程序性能下降或崩溃。以下是一些常见的内存泄漏原因:

  1. 全局变量
    如果不小心将变量定义为全局变量,可能会导致内存泄漏。全局变量在整个应用程序生命周期内都存在,无法被垃圾回收。

    function createGlobalVar() {
        leakedVar = "I'm a global variable"; // 没有使用 var/let/const 声明
    }
    createGlobalVar();
    
  2. 闭包
    使用闭包时,如果不当引用了外部变量,可能会导致不必要的内存占用。虽然闭包是 JavaScript 的一个强大特性,但也要注意避免过多引用。

    function createClosure() {
        let largeNumber = 100;
        return function() {
            console.log(largeNumber);
        };
    }
    const closureFunction = createClosure(); // largeNumber 仍然被 closureFunction 引用
    
  3. DOM 事件监听器
    如果事件监听器没有被移除,可能会导致内存泄漏,尤其是在动态创建和销毁 DOM 元素时。

    const button = document.createElement('button');
    button.addEventListener('click', () => {
        console.log('Button clicked');
    });
    // 如果不手动移除监听器,button 将保持在内存中
    
  4. 定时器和回调
    使用 setTimeoutsetInterval 时,如果不清除定时器,可能会导致内存泄漏。

    let intervalId = setInterval(() => {
        console.log('Interval running');
    }, 1000);
    // 如果不调用 clearInterval(intervalId),定时器将继续存在
    

预防内存泄漏的方法

  • 使用局部变量:尽量使用局部变量,避免不必要的全局变量。
  • 手动移除事件监听器:在不再需要时,及时移除事件监听器。
  • 清除定时器:在不需要时,使用 clearTimeoutclearInterval 清除定时器。
  • 避免循环引用:使用弱引用(如 WeakMapWeakSet)来避免循环引用的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值