Javascript内存管理

一、内存的生命周期

内存就是暂时存储程序以及数据的地方,任何一个程序的运行,都需要分配内存空间。内存的生命周期可以概括如下三个阶段:

内存的生命周期:

内存分配 ---> 使用内存 ---> 释放内存

例如C语言一般都有底层的内存接口,用来分配(malloc())、释放内存(free())。 对于JavaScript,创建变量的时候会自动分配内存,不再使用的时候会自动回收内存(这个过程被成为垃圾回收)。

1. JavaScript中内存分配

JavaScript在定义变量的时候就完成了内存分配。

1.1 栈内存、堆内存

JavaScript有七种数据类型:

  • 基本数据类型: String、Number、Null、Undefined、Boolean、Symbol

  • 引用数据类型: Object

基本数据类型,他们是固定大小的,保存在栈(Stack)空间。引用数据类型由于没有固定大小,所以放在堆(heap)中。

JS不允许直接操作堆栈,所以在操作引用数据类型时,实际上是在操作对象的引用。这里的引用就是指向堆内存中的内存地址。

为什么一定要分堆和栈?

因为JS需要用栈维护程序执行期间的上下文状态,如果所有数据都存在栈里,栈空间过大会影响上下文切换的效率,进而影响到程序的执行效率。

通常的策略,栈空间不会设置太大,主要存放一些原始类型的小数据。引用数据占空间都比较大,放在堆中,堆空间很大。缺点就是分配内存和内存回收都占用一定时间。

1.2 闭包

站在内存分配的角度,解释闭包:

闭包的字面特点是,嵌套函数,内部函数使用了外部函数的变量。

普通函数执行过程:

如果是普通函数,JS执行到函数比如A,首先会编译,新建一个空的执行上下文,对内部变量进行一次扫描。原始类型的数据会被压入栈中。函数A执行结束之后,A的上下文会被销毁,内部变量也一起销毁。

闭包的执行过程:

比如A内部嵌套B函数,函数B内部引用了A的变量。

函数执行到A照常进行编译,并且创建一个空上下文。 编译的过程遇到内部函数B,JS还需要对B进行一次词法扫描,当发现内部使用了A的变量,JS判定这是一个闭包,于是会在 堆空间中创建一个 “Closure(A)”对象用来保存变量。

JS会把所有使用的外部的变量都存在 Closure(A)中。

当A执行完毕被回收了,引用关系依然被保存在 Closure(A)中。

总结:

闭包产生的核心有两布:

  1. 需要扫描内部函数

  2. 内部函数引用外部变量保存到堆中

2. 内存使用

使用值的过程实际上是对分配内存进行读取与写入的操作。

读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。

const a = 10; // 分配内存
console.log(a); // 对内存的使用

2.1 不同类型的赋值

  • 基础数据类型的赋值是完整复制变量值

  • 引用类型赋值是复制引用地址

3. 内存回收 GC(Gabbage Collection)

原理是找出那些不再继续使用的内存,然后释放掉其所占用的内存,垃圾回收机制会按照一定的时间间隔周期性的执行这一操作。 js的内存管理是自动执行并且是不可见的,因为它的自动化和不可见导致它有优点也有缺点:

优点是它大幅的减少了内存管理的代码。同时也减少了因为长时间运行带来的内存泄漏问题。

缺点是不可控,js没有任何有关操作内存的api,无法干预内存管理。

3.1 什么是内存泄漏

当我们决定不再使用某些内存时,由于错误的编码,未能使得GC(Gabbage Collection)正确的将这些内存回收的情况,就是内存泄漏。

参考

Mark24

Everything can Mix.