JS内存管理-闭包的定义-内存模型-内存泄漏

64 阅读7分钟

内存管理

不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言会可以自动帮助我们管理内存:

不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期:

第一步:分配申请你需要的内存(申请);

第二步:使用分配的内存(存放一些东西,比如对象等);

第三步:不需要使用时,对其释放

手动管理内存:比如C、C++,包括早期的OC,都是需要手动来管理内存的申请和释放

自动管理内存:比如Java、JavaScript、Python、Swift、Dart等,它们有自动帮助我们管理内存;

基本数据类型:内存的分配会在执行时,直接在 空间进行分配

复杂数据类型:内存的分配会在 堆内存 中开辟一块空间,并且将这块空间的 指针 返回值变量引用

image.png

JS的垃圾回收

因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。

在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如free函数:

但是这种管理的方式其实非常的低效,影响我们编写逻辑的代码的效率;并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露;所以大部分现代的编程语言都是有自己的垃圾回收机制:

垃圾回收的英文是Garbage Collection,简称GC;对于那些不再使用的对象,我们都称之为是垃圾,它需要被回收,以释放更多的内存空间;

分代回收机制

v8 的垃圾回收机制基于分代回收机制,这个假说有两个特点,⼀是新⽣的对象容易早死,另⼀个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新⽣代和⽼⽣代。

新创建的对象或者只经历过⼀次的垃圾回收的对象被称为新⽣代

经历过多次垃圾回收的对象被称为⽼ ⽣代。

新⽣代被分为FromTo 两个空间,To ⼀般是闲置的。当From空间满了的时候会执⾏Scavenge 算法进⾏垃圾回收。当我们执⾏垃圾回收算法的时候应⽤逻辑将会停⽌,等垃圾回收结束后再继续执⾏。这 个算法分为三步:

(1)⾸先检查 From 空间的存活对象,如果对象存活则判断对象是否满⾜晋升到⽼⽣代的条件,如果满⾜条件则晋升到⽼⽣代。如果不满⾜条件则移动To空间。

(2)如果对象不存活,则释放对象的空间。

(3)最后将From空间和To空间⻆⾊进⾏交换。

新⽣代对象晋升到⽼⽣代有两个条件:

(1)第⼀个是判断是对象否已经经过⼀次Scavenge回收。若经历过,则将对象从From 空间复制到⽼⽣代中;若没有经历,则复制到 To 空间。

(2)第⼆个是 To 空间的内存使⽤占⽐是否超过限制。当对象从From 空间复制到To空间时,若 To空间使⽤超过 25%,则对象直接晋升到⽼⽣代中。

设置 25% 的原因主要是因为算法结束后,两个空间结 束后会交换位置,如果 To 空间的内存太⼩,会影响后续的内存分配。

⽼⽣代采⽤了标记清除法和标记压缩法。标记清除法⾸先会对内存中存活的对象进⾏标记,标记结束后 清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎⽚,不便于后⾯的内存分配。所以了解决内存碎⽚的问题引⼊了标记压缩法。 由于在进⾏垃圾回收的时候会暂停应⽤的逻辑,对于新⽣代⽅法由于内存⼩,每次停顿的时间不会太⻓,但对于⽼⽣代来说每次垃圾回收的时间⻓,停顿会造成很⼤的影响。为了解决这个问题 V8引⼊了增量标记的⽅法,将⼀次停顿进⾏的过程分为了多步,每次执⾏完⼀⼩步就让运⾏逻辑执⾏⼀会,就这样交替运⾏。

GC算法

  • 引用计数:当一个对象有一个引用指向它时,那么这个对象的引用就+1,当一个对象的引用为0时,这个对象就可以被销毁掉;这个算法有一个很大的弊端就是会产生循环引用;

  • 标记清除:这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象;这个算法可以很好的解决循环引用的问题;

标记清除法⾸先会对内存中存活的对象进⾏标记,标记结束后 清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎⽚,不便于后⾯的内存分配。所以为了解决内存碎⽚的问题引⼊了标记压缩法。

数组中的函数(方法)使用

方法:当我们的一个函数属于某一个对象时,则称这个函数是这个对象的方法

函数:独立的function

//过滤 filter :返回新的数组,只有为true才会放到新的数组中
var nums = [10,5,41,22,15]
var newnums  =  nums.filter((item, index,arr)=>{
   return 
   })
// 映射 map:返回新的数组,
// 迭代 forEach: 没有返回值 遍历
// 累加 reduce : 返回新的数组
nums.reduce(function(上一个的值,需要累加的值){
        return 上一次的值 + item
},0)
// 查找 find:整个对象
// 查找 findIndex :索引值

闭包

在计算机科学中对闭包的定义(维基百科):

  • 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures);

是在支持 头等函数 的编程语言中,实现词法绑定的一种技术;

  • 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表)(外层的函数变量);

  • 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量(外层作用域的变量) 会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;

闭包的概念出现于60年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么JavaScript中有闭包:

因为JavaScript中有大量的设计是来源于Scheme的;

我们再来看一下MDN对JavaScript闭包的解释:

  • 一个函数和对其周围状态(lexical environment,词法环境)(外层作用域)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure);

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;

那么我的理解和总结:

一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包;

从广义的角度来说:JavaScript中的函数都是闭包;

从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用于的变量,那么它是一个闭包

内存泄漏

如果要使用,一直存在没事,但是不再去使用了,依旧存在,则造成了内存的泄漏