持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
JS的内存
是网站性能的一个重要指标,很有必要学习一下内存的底层原理。那么要了解 JS 中的内存原理,我们首先应该要知道,内存中存储的是什么?
内存的数据存储
- 先回答上面的问题:内存中存的是什么? 应该是我们代码中定义的
数据
存在内存中。
那么数据以什么的形式怎么存在于内存中的呢?
栈和堆
两种内存结构划分:分为 栈内存
和 堆内存
。这种两种结构只是在概念上的划分,并不是物理层面上的划分。
两种内存结构的对比,如下图:
特点:
- 栈内存: 线性 连续的数据。
- 堆内存: 非线性 不连续,每个节点存储一些数据。
变量存储
- 基本数据类型 存在 栈内存中(数值 字符串 布尔 ...)
var a = 1;
var b = "str";
复制代码
上面代码定义的变量,应该是像下图这样存在栈内存
中的
- 引用数据类型存在堆内存(对象 数组 函数)
var obj = { a: 1};
var o2 = obj;
var fn = function(){ console.log(1); }
{ b: 100 }
复制代码
先来分析上面的第一行代码,var obj = { a: 1 }
,这段代码是怎么执行的呢?应该是先创建一个对象{ a: 1 }
,将这个对象存储在内存中,然后将这个对象所在的内存地址赋值给变量obj
。我们假设这个对象的内存地址是0x00000000
。
接下来第二行代码 var o2 = obj
,那么赋值给变量o2
的就是obj
的内存地址0x00000000
,并不是数据本身。所以当我们浅拷贝引用类型数据的时候,改变其中的一个变量另一个也会随之更改。这就是因为复制引用类型的变量,实际上被赋值的是数据的地址。
第三行代码创建fn
函数,也是同理,假设内存地址为0x00000005
。
第四行代码仅创建了一个对象,把它存在了堆内存中,假设内存地址为0x00000009
。这个对象没有赋值给任何变量,所以在栈内存中就没有关于这个对象的引用。
用画图来表示代码执行完的状态:
V8引擎
V8 指的是 Javascript的执行引擎,由C++
编写,采用即时编译 所以速度很快。
Node.js是用C ++编写。 Node.js是一个基于 Chrome V8 引擎的 JavaScript 运行环境,而V8 引擎使用C++开发,并在谷歌浏览器中使用。
V8的内存
在64位系统下V8引擎给我们提供1.4G的可用内存,32位系统下是700MB可用内存。
目前市场上大多数电脑也都是64位的操作系统了,那为什么V8为什么只给JavaScript提供1.4G内存?原因可能有以下两点:
-
因为 js 是脚本语言,一次执行就结束,1.4G完全够用。而java php等其他语言可能需要长期开启服务,所以这些语言可使用的内存量大一些。
-
js 的垃圾回收机制是自动回收的,而且是阻塞的(回收完垃圾再继续执行下面的代码)。自动回收会在内存快占满的时候回收,如果提供的内存量过大,回收的时候又是阻塞的可能占用很长的时间回收垃圾,导致使用程序的时候可能会出现卡顿,给用户不好的体验,所以定为1.4G。
我们所说的 1.4G 是V8的标准定义。在不同环境下,略有不同:
- 不同浏览器下,可能有一点点的扩容。
- 在Node环境下, js 可使用的内存接近于电脑的内存,因为
C++
的语言环境可以将电脑的内存提供给Node。
新生代和老生代
上面说到的64位操作系统(以下描述也都以64位操作系统为标准)下会提供 1.4G 内存,这1.4G 内存会被分为两个部分,称之为新生代 和 老生代。
特点:
- 新生代 大小约为 32MB 存放短时间存活的新变量
- 老生代 大小约为 1400MB 生存时间比较长的变量,会转存到老生代
新生代的回收算法
新生代的空间被一分为二,from 和 to 两个部分:
from 空间用来存放,to 空间永远是空着的。内存回收的时候,标记上可以回收的内容,然后将没有被标记的复制到to空间。
最后把 from 空间全部清空,把from 和 to对调。回收机制会反复的执行这样的过程,用图来表示这个过程:
这样做可以提升速度,牺牲空间换时间(减少了内存整理的时间)。
老生代回收
老生代的空间是一段连续的空间,内存回收分为下面三个步骤:
- 标记已死变脸
- 清除已死变量
- 磁盘整理
数组等一些数据必须存在连续的内存空间里,所以最后要进行磁盘整理,让内存中的空间是连续的。
新生代和老生代转化
-
触发转化的条件: 新生代发现本次复制后,也就是从 from 复制到 to 空间的时候,发现超过了 to 空间的 25%,就会触发转化。
-
当触发转化的条件发生后,会把哪些数据转进老生代呢?那些已经经历过一次新生代回收的,但是仍然存在的数据转进老生代。
基于代码分析
可以回收的状态
可以根据 是否存在引用关系,来看变量是否可以被回收。
{a: 1}
对象是有一个obj变量引用着它的,只要这个变量还在 内存中就要一直存着这个对象,也就是{a: 1}
不可被回收。
const obj = { a: 1 }
复制代码
- 如果直接这样定义一个对象,没有变量去引用它,就会标记可以回收,等到垃圾回收机制执行的时候,就会清除这个对象。
{ a: 1 }
复制代码
- 如果全局变量,那么会等这一段js代码执行完毕就会标记可以回收
// 全局
var a = 1;
复制代码
- 如果是局部变量,调用完这个方法就会标记可以回收(闭包函数是特殊情况)
function fn(){
var a = 1;
}
fn();
复制代码
- 闭包时,引用的内部变量不可被回收
function fn(){
var obj = {
a: 1
}
return function(){
console.log(obj);
};
}
var b = fn();
复制代码
上面fn
函数里边的obj
变量也是局部变量,var b = fn();
相当于外部的全局方法一直在引用着这个变量,所以不能被回收。
但如果直接执行fn()
,不把fn()
的返回值赋值给全局变量,那么就不存在引用关系,obj
变量就可以被回收。
触发垃圾回收的时机
- 执行完一次宏任务就会触发一次垃圾回收
var a = 1;
var b = 2;
console.log(a);
setTimeout(function(){
b++;
console.log(b);
// 第二次回收
},2000)
// 第一次回收
复制代码
- 内存不够的时候,在本次任务中就会触发垃圾回收
下面在node
环境下来演示:
定义获取当前内存的方法:
function getMemory () {
const _m = process.memoryUsage().heapUsed;
console.log(_m / 1024 / 1024 + "MB");
}
复制代码
创建大容量的数组占用内存测试:
var size = 30 * 1024 * 1024;
var a1 = new Array(size);
getMemory();
var a2 = new Array(size);
getMemory();
var a3 = new Array(size);
getMemory();
var a4 = new Array(size);
getMemory();
var a5 = new Array(size);
getMemory();
复制代码
每次创建完数组,就查看一次当前的内存。
控制台执行 node --max-old-space-size=1000 test.js
命令:
node
会自动扩容内存,所以手动设置node
的内存。使用指令:--max-old-space-size=1000
将node的老生代空间设置为1000MB。
看到控制台正常输出了当前占用的内存。
再多创建一个数组,存在全局变量中,再执行命令观察控制台就出现了报错信息:
var a6 = new Array(size);
getMemory();
复制代码
修改代码,将一部分变量改为局部变量,再执行命令,发现不会再报错
(function(){
var a3 = new Array(size);
getMemory();
var a4 = new Array(size);
getMemory();
var a5 = new Array(size);
getMemory();
})()
复制代码
分析输出的内容,能够了解到当执行到a5
的时候,发现内存不够了,就把局部变量a3 a4 a5
全部回收,所以到最后的时候内存还是723
。
优化的建议
- 尽量少的定义全局变量,适当的时机可以手动释放全局变量;
var size = 30 * 1024 * 1024;
var a1 = new Array(size);
// ...
a1 = null; // 释放
复制代码
- 注意代码中可能含有无限增长的变量的闭包
function f1(){
var arr = [];
f2 = function(){
console.log(arr);
}
return function (n) {
if(arr.length > 3){
arr.shift();
}
arr.push(n);
}
}
var a = f1();
a(1);
f2();
a(2);
f2();
a(3);
f2();
a(4);
f2();
a(5);
f2();
a(6);
f2();
复制代码
代码输出:
在无限增长的闭包中,要进行限制,否则占用内存过多无法回收,导致内存溢出。本例中使用if(arr.length > 3)
判断数组长度并进行处理,避免了这个问题。