JavaScript中内存的底层原理

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

JS的内存是网站性能的一个重要指标,很有必要学习一下内存的底层原理。那么要了解 JS 中的内存原理,我们首先应该要知道,内存中存储的是什么?

内存的数据存储

  • 先回答上面的问题:内存中存的是什么? 应该是我们代码中定义的 数据 存在内存中。

那么数据以什么的形式怎么存在于内存中的呢?

栈和堆

两种内存结构划分:分为 栈内存堆内存。这种两种结构只是在概念上的划分,并不是物理层面上的划分。

两种内存结构的对比,如下图:

捕获.PNG

特点:

  • 栈内存: 线性 连续的数据。
  • 堆内存: 非线性 不连续,每个节点存储一些数据。

变量存储

  • 基本数据类型 存在 栈内存中(数值 字符串 布尔 ...)
var a = 1;
var b = "str";
复制代码

上面代码定义的变量,应该是像下图这样存在栈内存中的

捕获.PNG

  • 引用数据类型存在堆内存(对象 数组 函数)
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。这个对象没有赋值给任何变量,所以在栈内存中就没有关于这个对象的引用。

用画图来表示代码执行完的状态:

捕获.PNG

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内存?原因可能有以下两点:

  1. 因为 js 是脚本语言,一次执行就结束,1.4G完全够用。而java php等其他语言可能需要长期开启服务,所以这些语言可使用的内存量大一些。

  2. js 的垃圾回收机制是自动回收的,而且是阻塞的(回收完垃圾再继续执行下面的代码)。自动回收会在内存快占满的时候回收,如果提供的内存量过大,回收的时候又是阻塞的可能占用很长的时间回收垃圾,导致使用程序的时候可能会出现卡顿,给用户不好的体验,所以定为1.4G。

我们所说的 1.4G 是V8的标准定义。在不同环境下,略有不同:

  • 不同浏览器下,可能有一点点的扩容。
  • 在Node环境下, js 可使用的内存接近于电脑的内存,因为C++的语言环境可以将电脑的内存提供给Node。

新生代和老生代

上面说到的64位操作系统(以下描述也都以64位操作系统为标准)下会提供 1.4G 内存,这1.4G 内存会被分为两个部分,称之为新生代老生代

特点:

捕获.PNG

  • 新生代 大小约为 32MB 存放短时间存活的新变量
  • 老生代 大小约为 1400MB 生存时间比较长的变量,会转存到老生代

新生代的回收算法

新生代的空间被一分为二,fromto 两个部分:

捕获.PNG

from 空间用来存放,to 空间永远是空着的。内存回收的时候,标记上可以回收的内容,然后将没有被标记的复制到to空间。

最后把 from 空间全部清空,把fromto对调。回收机制会反复的执行这样的过程,用图来表示这个过程:

捕获.PNG

这样做可以提升速度,牺牲空间换时间(减少了内存整理的时间)。

老生代回收

老生代的空间是一段连续的空间,内存回收分为下面三个步骤:

  1. 标记已死变脸
  2. 清除已死变量
  3. 磁盘整理

捕获.PNG

数组等一些数据必须存在连续的内存空间里,所以最后要进行磁盘整理,让内存中的空间是连续的。

新生代和老生代转化

  • 触发转化的条件: 新生代发现本次复制后,也就是从 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变量就可以被回收。

触发垃圾回收的时机

  1. 执行完一次宏任务就会触发一次垃圾回收
var a = 1;
var b = 2;
console.log(a);
setTimeout(function(){
  b++;
  console.log(b);
  // 第二次回收
},2000)
// 第一次回收
复制代码
  1. 内存不够的时候,在本次任务中就会触发垃圾回收

下面在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。

捕获.PNG

看到控制台正常输出了当前占用的内存。

再多创建一个数组,存在全局变量中,再执行命令观察控制台就出现了报错信息:

捕获.PNG

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();
})()
复制代码

捕获.PNG

分析输出的内容,能够了解到当执行到a5的时候,发现内存不够了,就把局部变量a3 a4 a5全部回收,所以到最后的时候内存还是723

优化的建议

  1. 尽量少的定义全局变量,适当的时机可以手动释放全局变量;
var size = 30 * 1024 * 1024;
var a1 = new Array(size);
// ...
a1 = null; // 释放
复制代码
  1. 注意代码中可能含有无限增长的变量的闭包
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();
复制代码

代码输出:

捕获.PNG

在无限增长的闭包中,要进行限制,否则占用内存过多无法回收,导致内存溢出。本例中使用if(arr.length > 3)判断数组长度并进行处理,避免了这个问题。

相关文章

JS专栏juejin.cn/column/7037…

猜你喜欢

转载自juejin.im/post/7108396905456992287