阅读 2311

Javascript 内存空间管理

原文地址:github.com/ruizhengyun…

我们都知道 Javascript 具有自动垃圾回收机制。一听到自动这个词,多好啊,能帮我们做点,我们就可以少做点事了。或许也部分是因为这个自动回收垃圾的机制,很多的前端小伙伴就减少了和内存空间打交道的场合或机会,就容易忽视这些。还有就是,前端开发人员许多都不是计算机专业毕业的,对内存空间的认知就比较模糊了,有的干脆就是一无所知。刚入行(前端)几年的我(一个文科生),就是一无所知的菜鸟,现在好多了,用我对象的话说抢了她(计算机专业毕业的)的饭碗。其实嘛,不知道没关系,可以学,那些计算机专业的小伙伴不也是从上大学开始学的嘛,咱只不过晚了点,如此而已。

基础知识很重要,别嫌我啰嗦,再强调一次。当然了,我肯定不是第一个,也不是最后一个这么啰嗦的。接下来的场景面试,你肯定深有体悟:平常在公司开发应用玩的很转,一面试,仿佛就在裸奔,许多知识讲的不彻底,自己都觉得虚。这个现象的本质还是在于基础知识的不扎实,所以在面试的时候,你想讲但是又不是很懂,或许你耍个聪明的去规避这些,可面试官也不傻(要是碰到傻得告诉我)。所以,请不要忽略基础知识

好了,接下来好好说说基础知识之内存空间到底是怎么个玩法。这一次,一定要整个明白(给自己立个 Flag)。

先理解三种数据结构

它们分别是堆(heap)、栈(stack)、队列(queue),一图知所有。

Javascript 没有像 C/C++ 严格意义上区分堆内存和栈内存。我们可以简单粗暴一刀切(就这个意思了)的理解 Javascript 数据存在堆内存中。但是,重点来了,有些场景还是需要栈数据结构的思维来理解的,比如执行上下文,存放变量。

从上图一眼可看出其特点 后进先出(LIFO)(走后门啊这是),这就是栈的存储原理,一个有后台的数据结构

这里有个的概念,即存放常量,所以也叫常量池。

一种树状结构。好比 JSON 格式中的数据,你有 key,我有对应的 value, 就立马返给你。一个绝对公平的数据结构

只要你要,只要我有。

队列

Javascript 中,队列数据结构的应用主要体现在事件循环机制(eventLoop) 上。其特点很直白就是先进先出(FIFO),和栈结构正好相反,一个相对公平的数据结构

先到先得

数据类型与数据结构

Javascript 执行上下文后,会创建一个叫变量对象的特殊对象,其实这个对象也存在堆内存中,但由于特殊,要与堆内存区分。

基础数据类型

基础数据类型都是一些简单的数据段,包括 Undefined、Null、Boolean、Number、String 和 Symbol。保存在栈内存中,因为这些类型在内存中分别占有固定大小的空间,通过按值访问,所以可直接操作变量中的值,即寻值

引用数据类型

引用数据类型的值保存在堆内存的对象中。因为这类值的大小不固定,因此不能把它们保存到栈内存中,但内存地址大小的固定的,因此保存在堆内存中,在栈内存中存放的只是该对象的访问地址。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值,即寻址

闭包中的变量并不保存中栈内存中,而是保存在堆内存中,这也就解释了外层函数弹出调用栈后为什么闭包还能引用到函数内的变量。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

理解方式

var name = 'pr'; // 变量对象
var age = 30; // 变量对象

var info = { name: 'pr' }; // 变量 info 存在变量对象中,{ name: 'pr' } 作为对象存在堆内存中
var relation = [1, 2, 3]; // 变量 relation 存在变量对象中,[1, 2, 3] 作为对象存在堆内存中
复制代码

因此当要访问 inforelation 这些对内存的引用数据类型时,实际上是从变量对象中获取其地址引用,然后才从堆内存获得数据。

举例子

问题1

var name = 'pr';
var nickName = name;
nickName = '一如既往如我';
console.log(name); // pr
复制代码

问题2

var info = { name: 'pr' };
var nicInfo = info;
nicInfo.name = '一如既往如我';
console.log(info.name); // 一如既往如我
复制代码

问题3

var info = { name: 'pr' };
var nicInfo = info;
nicInfo = null
console.log(info.name); // pr
复制代码

上面3个例子,相信这次你应该明白了。在变量数据中发生了数据复制。基本数据类型和引用数据类型的复制过程图可看下面

对于问题1,基础类型复制过程

对于问题2,引用类型复制过程

对于问题3,null 是基本类型,所以同问题1,并不会影响堆内存中的对象,所以 info 不受影响,你学会了么?

总结

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

内存空间的管理

本文一开始就提到 Javascript 具有自动垃圾回收机制,所以前端开发就没关注过内存的使用问题。但是,了解内存机制有助于知道编写的代码在执行过程中发生了什么,从而提高编程质量。

生命周期

  • 分配内存
  • 使用内存(读写)
  • 释放内存

内存回收

1.局部变量和全局变量的销毁

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

2.以 Google 的 V8 引擎为例,V8 引擎中所有的 JS 对象都是通过堆来进行内存分配的

  • 初始分配:当声明变量并赋值时,V8引擎就会在堆内存中分配给这个变量;
  • 继续申请:当已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止;

3.V8引擎对堆内存中的 JS 对象进行分代管理

  • 新生代:存活周期较短的 JS 对象,如临时变量、字符串等;
  • 老生代:经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等;

垃圾回收算法

对垃圾回收算法来说,核心思想就是如何判断内存已经不再使用,常用垃圾回收算法有下面两种。

  • 引用计数(现代浏览器不再使用)
  • 标记清除(常用)

引用计数

引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明该对象已经不再需要了。

let info = {
    name: 'pr',
    age: 30
};
let nickInfo = info;
info = '一如既往如你';
nickInfo = null;
复制代码

引用计数有一个致命的问题,那就是循环引用

function cycleFn() {
    let o1 = {};
    let o2 = {};
    o1.obj = o2;
    o2.obj = o1;
}
cycleFn();
复制代码

cycle 函数执行完成之后,对象 o1o2 实际上已经不再需要了,但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收。所以现代浏览器不再使用这个算法。但是 IE 依旧使用

let createElementDiv = document.createElement("div");
createElementDiv.onclick = function() {
    console.log("点击创建的元素 div", createElementDiv);
};
复制代码

上面的例子就是一个循环引用。变量 createElementDiv 有事件处理函数的引用,同时事件处理函数也有 createElementDiv 的引用,因为 createElementDiv 变量可在函数内被访问,所以循环引用就出现了。

标记清除

标记清除算法将“不再使用的对象”定义为“无法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。所以上面的例子就可以正确被垃圾回收处理了。

var name = 'pr';
console.log(name);
name = null;
复制代码

上面 name = null 做了释放引用,脱离执行环境,这个值在下次垃圾收集器执行时释放。所以,适当时候解除引用,是页面性能提升的一个好的方式。

内存泄漏

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

1、浏览器方法

  • 打开开发者工具,选择 Memory
  • 在右侧的Select profiling type字段里面勾选 timeline
  • 点击左上角的录制按钮。
  • 在页面上进行各种操作,模拟用户的使用情况。
  • 一段时间后,点击左上角的 stop 按钮,面板上就会显示这段时间的内存占用情况。

2、命令行方法 使用 Node 提供的 process.memoryUsage 方法。

node
> process.memoryUsage()

// 输出
{ rss: 23904256,
  heapTotal: 7331840,
  heapUsed: 5042520, // 用到的堆的部分,判断内存泄漏,以这个字段为准。
  external: 20601 
}
复制代码
关注下面的标签,发现更多相似文章
评论