JavaScript内存空间图解

923 阅读6分钟

栈数据结构

栈, 计算机专业的同学可能都有些了解. 讲究的是一个先进后出. 今天我们简单的来过一下.

可以想象为只有一个口子的一条路, 这条路. 每次呢只进去一个人. 后面的人进来的, 只有等后面的人出去了, 先进去的才可以出来. 所以就是 先进后出

堆数据结构

堆数据结构是一种树状结构。它的存取数据的方式与书架和书非常相似。我们只需要知道书的名字就可以直接取出书了,并不需要把上面的书取出来。JSON格式的数据中,我们存储的key-value可以是无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。

队列

队列是一种先进先出(FIFO)的数据结构,这是事件循环(Event Loop)的基础结构,事件循环我们有空的话, 可能会讲一下.

变量的存放

这个问题是一个老生常谈的问题了, 会引发出深拷贝之类的问题.今天我做一个简单的介绍吧.

首先我们应该知道内存中有栈和堆,那么变量应该存放在哪里呢,

  • 1、基本类型 --> 保存在内存中,因为这些类型在内存中分别占有固定大小的空间,通过按值来访问。基本类型一共有6种:Undefined、Null、Boolean、Number 、String和Symbol(ES6
  • 2、引用类型 --> 保存在内存中,因为这种值的大小不固定,因此不能把它们保存到栈内存中,但内存地址大小的固定的,因此保存在堆内存中,在栈内存中存放的只是该对象的访问地址。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。
  • 画图

在计算机的数据结构中,栈比堆的运算速度快,Object是一个复杂的结构且可以扩展:数组可扩充,对象可添加属性,都可以增删改查。将他们放在堆中是为了不影响栈的效率。而是通过引用的方式查找到堆中的实际对象再进行操作。所以查找引用类型值的时候先去查找再去查找。

问题时间

var a = 20;
var b = a;
b = 30;

// 这时a的值是多少?
var a = { name: '前端开发' }
var b = a;
b.name = '进阶';

// 这时a.name的值是多少
var a = { name: '前端开发' }
var b = a;
a = null;

// 这时b的值是多少
function foo() {
    foo();
}
foo();

// 会发生什么?

ES6语法中的 const 声明一个只读的常量,那为什么下面可以修改const的值?

  • 对于问题1,a、b都是基本类型,它们的值是存储在栈中的,a、b分别有各自独立的栈空间,所以修改了b的值以后,a的值并不会发生变化。
  • 对于问题2,a、b都是引用类型,栈内存中存放地址指向堆内存中的对象,引用类型的复制会为新的变量自动分配一个新的值保存在变量对象中,但只是引用类型的一个地址指针而已,实际指向的是同一个对象,所以修改b.name的值后,相应的a.name也就发生了改变。
  • 对于问题3,首先要说明的是null是基本类型,a = null之后只是把a存储在栈内存中地址改变成了基本类型null,并不会影响堆内存中的对象,所以b的值不受影响。
  • 下面的递归就会无限制的执行下去,直到超出调用堆栈的实际大小,这个是浏览器定义的。(如果不限制大小的话, 垃圾回收机制执行就要很久影响性能

内存空间管理

JavaScript的内存生命周期是

  • 1、分配你所需要的内存
  • 2、使用分配到的内存
  • 3、不需要时将其释放、归还

=>

垃圾回收机制

OK, 言归正传 这里只简单介绍一下V8引擎 V8引擎把内存分为两块

  • 新生代算法
  • 老生代算法

新生代算法(scavenge)

在分代的基础上,新生代用的是scavenge算法,再具体实现是用cheney算法,它把内存空间一分为二,每一个叫做semispace,这两个semispace一个处于使用,一个处于闲置,处于使用的叫做From,处于闲置的叫做To,赋值时先分配到From,当开始进行垃圾回收时,还在被使用的变量会被复制到To,否则会被直接释放掉,然后From和To互换位置,在开始下一次垃圾回收机制时,如果还被使用则晋升为老生代,或者占用空间大于25%。

它的缺点是只能使用堆内存的一半,这是一个典型的空间换时间算法,但新生代声明周期较短,恰恰就适合这个算法。

老生代算法(mark-sweep & mark-compact)

通过标记清除的算法来找到哪些对象是不再继续使用的,使用a = null其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。

在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在开发中,需要尽量避免使用全局变量。

内存泄漏

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

最常见的就是计时器setInterval()addEventListener

补充

补充一个知识点,就是闭包中的变量并不保存中栈内存中,而是保存在堆内存中,这也就解释了函数之后之后为什么闭包还能引用到函数内的变量。

闭包的简单定义是:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

OK, 点到为止, 因为我也不大了解, 哈哈

参考

muyiy.cn/blog/1/1.3.…