内存管理和内存泄露(闭包、作用域链)(三)

简介: 内存管理和内存泄露(闭包、作用域链)

内存管理和内存泄露(闭包、作用域链)(二)https://developer.aliyun.com/article/1470366

闭包的执行过程

以下是我们已经非常熟悉的闭包过程,这次我们来看下他是怎么进行执行的,这次会解开我们之前还不了解的,为什么闭包会让本该执行完的执行上下文的自由变量不会被销毁掉

执行之前一样是非常熟悉的流程,直接上图啦

image.png

当foo开始执行之后

image.png

当foo执行完了之后:这个时候,bar的内存地址已经存放到fn中了(也就是fn已经指向bar了),并且在后续被fn()给调用了,所以不管foo的函数执行上下文有没有被销毁,都不会影响到bar的函数对象了(因为GO根对象的fn已经指向了bar函数对象了上面有介绍JavaScript的垃圾回收,也就是标记清除部分,让bar函数对象不被销毁),然后bar函数对象连锁反应又跟foo的AO对象相互进行引用了(最关键的是bar指向foo的AO对象,这是可达的部分),所以foo的AO对象也不会被销毁。这就是为什么bar引用的父级自由变量会得以保留的原因

image.png

我们接下来就要继续执行fn的函数执行上下文(bar的)了

image.png

image.png

当bar的执行上下文被销毁掉的时候,也不会影响闭包,因为根对象依旧指向着fn,也就是bar的函数对象,而bar函数对象的父级作用域parentScope指着foo的AO对象,所以脱离了捕捉时的上下文,它也能照常运行。自由变量依旧存在而没有被销毁

image.png

function foo(){
    var name = "xiaoyu"
    var age = 20
    function test(){
        console.log("这是我的名字",name);
        console.log("这是我的年龄",age);
    }
    return test
}
var fn = foo()
fn()
//这是我的名字 xiaoyu
//这是我的年龄 20

04_函数执行作用域链和深入闭包

闭包的内存泄漏

从上面的代码块中,我们可以知道,当bar函数不被销毁的时候,foo的AO对象就永远不会被销毁,因为我们bar要访问foo的AO对象里面的内容


  • 目前因为在全局作用域下fn变量对0xb00的函数对象有引用,而0xb00的作用域中AO(0x200)有引用,所以会造成这些内存都是无法被释放的


但如果我们的bar函数只执行一次,后面就再也不需要了,那这个AO对象一直保存着就没有意义了,该销毁的却一直保留着,我们就叫这个是内存泄漏

闭包内存泄漏案例

image.png

image.png

  • 只要arrayFns数组不被销毁,则createFnArray函数也会一直保留着不被销毁

V8引擎源码可以看到对数字的处理:(是在后面回顾的时候进行补充说明的)

image.png

function createFnArray(){
    // 创建一个长度为1024*1024的数组,往里面每个位置填充1.观察占了多少的内存空间(int类型,整数1占4个字节byte)
    //4byte*1024=4kb,再*1024为4mb,占据的空间是4M × 100 + 其他的内存 = 400M+
    //在js里面不管是整数类型还是浮点数类型,看起来都是数字类型,这个时候占据的都是8字节,但是js引擎为了提高空间的利用率,对很多小的数字是用不到8个字节(byte)的,8字节 = 2的64次方,所以8字节是很大的,现在的js引擎大多数都会进行优化,对小的数字类型,在V8中称为Smi,小数字 2的32次方
    var arr = new Array(1024*1024).fill(1)
    
    return function(){
        console.log(arr.length);
    }
}
//var arrayFn = createFnArray()
//arrayFn()
var arrayFns = []
for(var i = 0 ; i<100 ; i++){
    //createFnArray()//我们通过for循环不断调用createFnArray这个函数,我们没有使用任何函数去接收他,所以当他创建进入下一个循环之后就会马上被销毁掉
    arrayFns.push(createFnArray())
}

image.png

内存泄漏解决方法

image.png

//内存泄漏解决方法
function foo(){
    var name = "xiaoyu"
    var age = 20
    function test(){
        console.log("这是我的名字",name);
        console.log("这是我的年龄",age);
    }
    return test
}
var fn = foo()
fn()
fn = null//将fn指向null,null的内存地址为0x0。此时fn指向bar的指针就会断开了,AO对象跟bar函数对象就形成了一个对于根对象的不可达的对象,将再下次被销毁掉。注意,你把它置为null之后,不会马上回收的,会在发现之后的下一轮进行回收

AO不使用的属性

  • 我们来研究一个问题:AO对象不会被销毁时,是否里面的所有属性都不会被释放?
  • 下面代码中的name属于闭包的父作用域里面的变量
  • 我们知道形成闭包之后count一定不会被销毁掉,那么name是否会被销毁掉呢?会,没有被使用到的会销毁掉,V8引擎做的优化
function makeAdder(count){
    let name ="why" 
    return function (num){
        debugger
        return count + num
    }
}
const add10 = makeAdder(10)
console.log(add10(5));
console.log(add10(8));
//15
//18

闭包的内存泄漏测试

内存回收案例测试如下

image.png

如果我们连foo函数对象都不想要了,我们也来个foo = null,断掉了foo与根对象GO的联系,那下次foo函数也会被销毁,或者说垃圾回收掉


image.png

回收一半的内存

JS闭包引用的自由变量销毁

当我们除了声明了fn来接收foo()之外,又声明了baz同样子接收foo(),这个时候是又执行了一遍foo函数里面的bar部分,fn跟baz不是同时指向同一个地方,而是又创建了一个新的foo的AO对象跟bar的函数对象,当我们将fn指向null,将内存进行回收时的时候,销毁的也只是fn对应的bar函数对象跟foo()对象,而对baz产生的bar函数对象跟foo的AO对象没有任何的影响,毕竟baz是又重新走了一遍流程,baz跟fn是互相独立的(PS:foo的AO对象是由bar的父级作用域内存地址指向而产生出来的)

image.png

  1. foo的AO对象有bar在指向着,因为bar函数内含父级作用域foo的AO对象的内存地址且正处于引用状态,这个内存地址指向着AO对象,让AO对象不会被销毁掉,但是我们只是引用name这个自由变量,age并没有使用到,按照ECMA规范,正规AO对象都不会被销毁,当然也就包含了我们没有用上的age变量了
  2. 但是js引擎是非常灵活的,为了提高内存的利用率,这个可能永远使用不上的age属性是会被回收掉的,从而提高空余的内存空间,提高性能

image.png

闭包引用的AO对象属性销毁

通过debugger我们可以看到未使用的父级作用域的变量会被js引擎回收掉,如果引用了则不会

function foo(){
    var name = "why"
    var age = 18
    function bar(){
        debugger
        console.log(name)
    }
    return bar
    
}
var fn = foo()
fn()

image.png

目录
相关文章
|
30天前
|
自然语言处理 JavaScript 前端开发
内存管理和内存泄露(闭包、作用域链)(二)
内存管理和内存泄露(闭包、作用域链)
28 0
|
3月前
|
存储 JavaScript 前端开发
|
30天前
|
存储 JavaScript 前端开发
内存管理和内存泄露(闭包、作用域链)(一)
内存管理和内存泄露(闭包、作用域链)
58 0
|
1月前
|
存储
内存管理之内存释放函数
内存管理之内存释放函数
15 0
|
2月前
|
缓存 自然语言处理 JavaScript
10分钟带你深入理解JavaScript的执行上下文和闭包机制
JavaScript中的闭包源于计算机科学中的一种理论概念,称为“λ演算”(Lambda Calculus)。λ演算是计算机科学的基础之一,1930年由Alonzo Church提出,它是一种用于描述计算过程的数学抽象模型,也是函数式编程语言的基础。
|
9月前
|
存储 JavaScript 前端开发
JS进阶(三) 闭包,作用域链,垃圾回收,内存泄露
闭包,作用域链,垃圾回收,内存泄露 1、函数创建 创建函数 1、开辟一个堆内存(16进制的内存地址) 2、声明当前函数的作用域(再哪个上下文创建的,它的作用域就是谁) 3、把函数体内的代码当作字符串存储在堆内存当中(所以不执行没有意义) 4、把函数的堆内存地址类似对象一样放到栈中供对象调用 执行函数 1、会形成一个全新的私有上下文(目的是供函数中的代码执行),然后进栈执行 2、在私有上下文中有一个存放私有变量的变量对象 AO(xx) 3、在代码执行之前要做的事情 - 初始化它的作用域链<自己的上下文,函数的作用域> - 初始化this (箭头函数没有this) - 初始化Arguments实参
65 0
|
10月前
|
存储 JavaScript 前端开发
从执行上下文和作用域链理解闭包
从执行上下文和作用域链理解闭包
74 0
从执行上下文和作用域链理解闭包
|
存储 JavaScript 前端开发
一篇文章带你搞定javaScript变量作用域和内存问题(变量,作用域,垃圾收集,管理内存)
一篇文章带你搞定javaScript变量作用域和内存问题(变量,作用域,垃圾收集,管理内存)
58 0
|
JavaScript Java
面试题:闭包、作用域链、内存泄漏
面试题:闭包、作用域链、内存泄漏
什么是闭包?闭包的用途是什么?闭包的缺点是什么?
变量的作用域有两种:全局变量和局部变量; 函数内部可以直接读取全局变量; 在函数外部无法读取函数内的局部变量。 能够读取其他函数内部变量的函数,就是闭包
88 0