阅读 3244

容易被遗忘的前端基础:JavaScript 内存详解

目录

JS内存目录

简介

某些语言,比如C有低级的原生内存管理原语,像malloc()和free()。开发人员使用这些原语可以显式分配和释放操作系统的内存。

相对地,JavaScript会在创建变量(对象、字符串)时自动分配内存,并在这些变量不被使用时自动释放内存,这个过程被称为垃圾回收。这个“自动”释放资源的特性带来了很多困惑,让JavaScript(和其他高级级语言)开发者误以为可以不关心内存管理。这是一个很大的错误

内存生命周期

无论使用什么编程语言,内存生命周期基本是一致的:

内存生命周期图

  • 分配内存: 内存被操作系统分配,允许程序使用它 (当申明变量、函数、对象的时候,系统会自动为他们分配内存)
  • 使用内存:通过在代码操作变量对内在进行读和写 (也就是使用变量、函数等)
  • 释放内存:不用的时候,就可以释放内存,以便重新分配 (由垃圾回收机制自动回收不再使用的内存)

JS 内存模型

在JavaScript中的内存空间分为两种:栈内存(stack)堆内存(heap), 而JavaScript的数据类型也分为两大类, 分别是基本数据类型引用数据类型。 这些数据类型在内存中是怎样存储的?

说是JS内存模型其实不太准确,只是便于理解。由于JavaScript中的内存分配是由js引擎完成的,所以更准确的描述是js引擎的内存模型。
复制代码

基础数据类型与栈内存

JS中的基础数据类型,这些值都有固定的大小,往往都保存在栈内存中(闭包除外),由系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。

所熟知的基础数据类型:

NumberString、Null、Boolean、Undefiend、Symbol(ES6新增)
复制代码

简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图左侧。

栈存取方式

这种乒乓球的存放方式与栈中存取数据的方式如出一辙。处于盒子中最顶层的乒乓球5,它一定是最后被放进去,但可以最先被使用。而我们想要使用底层的乒乓球1,就必须将上面的4个乒乓球取出来,让乒乓球1处于盒子顶层。这就是栈空间先进后出,后进先出的特点。

引用数据类型与堆内存

与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以粗浅地理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。

堆数据结构是一种树状结构。它的存取数据的方式,则与书架与书非常相似。

所熟知的引用数据类型:

ObjectArrayDateRegExpFunction 等。
复制代码

为了更好的搞懂变量对象与堆内存,我们可以结合以下例子与图解进行理解。

var a1 = 0;   // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象

var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中
复制代码

Javascript内存详解-2

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。

理解了JS的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的一些特点了。

接下来,我们通过下面的例子来加深对JS内存的理解

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

var m = { a: 10, b: 20 };
var n = m;
n.a = 15; 
复制代码

此时a的值是什么? 而m.a的值又是什么?

Javascript内存详解-3

在变量对象中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。

Javascript内存详解-4

通过var n = m执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在变量对象中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在变量对象中访问到的具体对象实际上是同一个。

垃圾回收

垃圾回收是一种内存管理机制,就是将不再用到的内存及时释放,以防内存占用越来越高,导致卡顿甚至进程崩溃。在JavaScript中有垃圾回收机制,其作用就是自动回收过期无效的变量。

在JavaScript中内存垃圾回收是由js引擎自动完成的。实现垃圾回收的关键在于如何确定内存不再使用,也就是确定对象是否无用。主要有两种方式:引用计数 和 标记清除。

垃圾回收算法

引用计数(reference counting)

这是IE6、7采用的一种比较老的垃圾回收机制。这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

var o1 = {
  o2: {
    x: 1
  }
};

//2个对象被创建
/'o2''o1'作为属性引用
//谁也不能被回收

var o3 = o1; //'o3'是第二个引用'o1'指向对象的变量

o1 = 1;      //现在,'o1'只有一个引用了,就是'o3'
var o4 = o3.o2; // 引用'o3'对象的'o2'属性
                //'o2'对象这时有2个引用: 一个是作为对象的属性
                //另一个是'o4'

o3 = '374'; //'o1'原来的对象现在有0个对它的引用
             //'o1'可以被垃圾回收了。
            //然而它的'o2'属性依然被'o4'变量引用,所以'o2'不能被释放。

o4 = null;  //最初'o1'中的'o2'属性没有被其他的引用了
           //'o2'可以被垃圾回收了
复制代码

循环引用创造麻烦 在涉及循环引用的时候有一个限制。在下面的例子中,两个对象被创建了,而且相互引用,这样创建了一个循环引用。它们会在函数调用后超出作用域,应该可以释放。然而引用计数算法考虑到2个对象中的每一个至少被引用了一次,因此都不可以被回收。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 引用 o2
  o2.p = o1; // o2 引用 o1\. 形成循环引用
}

f();
复制代码

Javascript内存详解-6

标记清除(mark and sweep)

工作原理简化后就是:从垃圾收集根(root)对象(在JavaScript中为全局环境记录)开始,标记出所有可以获得的对象,然后清除掉所有未标记的不可获得的对象。

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。 算法包含以下步骤。

  • 垃圾回收器生成一个根列表。根通常是将引用保存在代码中的全局变量。在JavaScript中,window对象是一个可以作为根的全局变量。
  • 所有的根都被检查和标记成活跃的(不是垃圾),所有的子变量也被递归检查。所有可能从根元素到达的都不被认为是垃圾。
  • 所有没有被标记成活跃的内存都被认为是垃圾。垃圾回收器就可以释放内存并且把内存还给操作系统。

Javascript内存详解-7

2012年起,所有浏览器都内置了标记清除垃圾回收器。

内存泄漏

内存泄漏基本上就是不再被应用需要的内存,由于某种原因,没有被归还给操作系统或者进入可用内存池。 简单来说: 就是不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

Chrome 浏览器查看内存占用

按照以下步骤操作

  • 打开Chrome浏览器开发者工具的Performance面板
  • 选项栏中勾选Memory选项
  • 点击左上角录制按钮(实心圆状按钮)
  • 在页面上进行正常操作
  • 一段时间后,点击Stop,观察面板上的数据

Javascript内存详解-8

更多方式查看内存占用,点击这里

4种常见的JavaScript内存泄漏

    1. 意外的全局变量
    1. 被遗忘的定时器或者回调
    1. 闭包
    1. DOM外引用

闭包本身不会造成内存泄露,程序写错了才会造成内存泄漏或者闭包过多很容易导致内存泄漏。

具体详情点击【译】JavaScript是如何工作的:内存管理 + 如何处理4个常见的内存泄露

总结

JS内存详解

JS内存详解

原文地址

参考资料

关注下面的标签,发现更多相似文章
评论