zhangzhibang0309/__Blog

聊聊JavaScript内存管理之垃圾回收与闭包

Closed this issue · 0 comments

内存管理

一.认识内存管理

  • 所有编程语言都需要分配内存,但是有些语言需要我们手动管理,而有些是自动管理。
  • 不管以什么养的方式来管理内存,内存的管理都会有如下的生命周期:
    • 第一步:分配申请你需要的内存(申请);
    • 第二步:使用分配的内存(存放一些东西,比如对象等);
    • 第三步:不需要使用时,对其进行释放;
  • 不同编程语言的不同实现:
    • 手动管理:c/c++ oc 需要手动管理内存的申请和释放(malloc 和 free)
    • 自动管理:java JavaScript python swift dart等等

(一)js的内存模型

二.js的垃圾回收

  • 因为内存大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,腾出更多空间。
  • 手动内存管理编码效率低效,而且要求很高,容易内存泄露。
  • 所以现代编程语言都有自己的垃圾回收机制——Garbage Collection(GC)
    • 对于不再使用的对象,我们都称之为垃圾。
    • 对于java来说,jvm里面有自己的垃圾回收机制;对于JavaScript来说,垃圾回收js引擎帮助我们实现。
  • gc判断是否为垃圾,就是判断这个对象是否还有引用,这就导致了一些问题:
    • 如果两个对象之间互相引用,那么二者虽然没有其他地方在使用,但还是一直不能被回收。
      • 解决办法 - 标记清除:设置一个根对象,垃圾回收期会从根开始,找到所有从根开始的有引用的对象,没有引用到的对象,就被认为是垃圾。这个算法很好的解决了循环引用的问题。
      • 20211128195431

三.高阶函数

image-20211130122402305
如图所示,这就是函数作为第一公民,产生的高阶函数用法。

四.闭包

  • 在计算机科学领域:
    • 闭包跟函数的最大的区别在于 当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,他也能照常运行。
    • 闭包在实现上是一个结构体,他存储了一个函数和一个关联的环境(相当于一个符号查找表)
    • 是在支持头等函数的编程语言中,实现词法绑定的一种技术。
    • 此法闭包或者函数闭包。
  • MDN:
    • 一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。
    • 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
    • 在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • WHY -- 对于闭包一些争论的定义:
    • 一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包。
    • 从广义的角度来说:JavaScript中的函数都是闭包;
    • 从狭义的角度来说:JavaScript中的一个函数,如果访问了外层作用域的变量,那么他是一个闭包。

闭包在v8中的执行过程:

function foo() {
  var name = "zzb";
  function bar() {
    console.log("bar", name);
  }

  return bar;
}

var fn = foo();
fn();

var name = "zzb";
function demo() {
    console.log(name)
}

// 可以访问:test就是闭包
// 有访问到:test就不是闭包,因为他虽然能访问,但是他没有访问
function test() {
    
}

(一)闭包在内存是怎样实现的?(闭包函数在内存中的执行过程)

先说一下没有严格闭包的函数的内存执行过程

function foo() {
  var name = "zzb";
  function bar() {
    console.log("bar", name);
  }

  return bar;
}

var fn = foo();
fn();

var name = "zzb";
function demo() {
    console.log(name)
}

// 可以访问:test就是闭包
// 有访问到:test就不是闭包,因为他虽然能访问,但是他没有访问
function test() {
    
}

这个比较普通,就是看一下内存里面销毁的情况。

只要还有指针指向,他就不会销毁,指针源头来自哪里呢,GO,GO是全局对象,程序没结束不会销毁,被它指向的东西不会销毁。

20211203214906

然后看看形成闭包的函数在内存中执行过程,主要是为什么外部的函数变量对象不会被销毁。

function foo() {
  var name = "foo";
  var age = 18;

  function bar() {
    console.log(name);
    console.log(age);
  }
  return bar;
}

var fn = foo();
fn();

fn = null;
foo = null;

原因很简单,go中一直有变量指向foo bar两个函数对象,而且这俩对象又指向parentscope,所以内层函数对象存在,其外层函数变量对象不会销毁。

(二)js闭包内存泄露案例

function createFnArray() {
  // js整数占据4B
  // 占据的空间是4M
  var arr = new Array(1024 * 1024).fill(1);
  return function () {
    console.log(arr.length);
  };
}

var arrayFns = [];
for (var i = 0; i < 100; i++) {
  setTimeout(() => {
    arrayFns.push(createFnArray());
  }, i * 100);
}

setTimeout(() => {
  for (var i = 0; i < 50; i++) {
    arrayFns.pop();
  }
}, 10000);

20211204142444

在上面所展示的案例中,可以看出来arr这个数组不会被销毁,而且这种情况下,你把所有调用函数返回值传给一个数组一个个存起来,他们的arr是会多次创建的,并不都是引用的同一个。(我觉得这里可能牵扯到一点深浅拷贝的东西,所以这里还是有点疑惑,但是不影响理解闭包产生的内存泄露)

然后这张图展示了arr所在的AO为什么不会被销毁。

下面实在浏览器devtool里面进行的测试,上面的代码是创建100个4m的数组,然后销毁一般,浏览器展示的内存反映了这个结果。

(三)js闭包引用的自由变量销毁

function foo() {
  var name = " why";
  var age = 18;

  function bar() {
    // 只用了name 而没用age 内存中的闭包会不会销毁age呢
    // 答案是会的 虽然ecma规范中,age按理来说也应该存在于AO中,AO没有被销毁,age自然不会销毁
    // 但是规范是规范 v8实现的时候是很灵活的 会将AO中没用到的变量内存给销毁
    console.log(name);
  }
}