likes
comments
collection
share

深入JavaScript之内存管理

作者站长头像
站长
· 阅读数 24

JavaScript的内存管理是自动进行的,在创建变量(对象,字符串等)时自动进行了内存分配,之后在代码执行,使用变量时占用这个内存,当不再使用变量后就内存会被回收,释放掉。在JavaScript中,这个过程被称为垃圾回收机制。

什么是内存

计算机硬件由5个部分组成:控制器运算器存储器输入设备输出设备。通常我们所说的内存属于存储器,在程序运行时,cpu需要的调用指令和数据只能从内存中获取(硬盘只有存储功能,执行时会将数据缓存到内存中)。JavaScript只是一种语言,真正进行内存的调用和分配的是JavaScript引擎。

内存的生命周期

不管什么语言,内存的生命周期基本是一致的,一般为以下几个阶段:

  1. 分配你所需要的内存。
  2. 使用分配到的内存(读,写)。
  3. 不需要时将其释放,归还

JavaScript语言中,第一步和第三步是JavaScript引擎自动进行的。

JavaScript引擎

JavaScript引擎是什么

JavaScript引擎是一个专门处理JavaScript脚本的虚拟机。它本质上就是一段程序,可以将JavaScript代码编译为不同CPU对应的汇编代码,此外还负责执行代码,分配内存和垃圾回收等等。

JavaScript引擎的内存结构

JavaScript引擎的内存结构可以粗略分为两个部分:栈(Stack)堆(Heap)。现在市面上比较流行的JavaScript引擎有Google的v8引擎、Apple的JavaScriptCore等等。不同的引擎它的内存结构有所差别,之后会对v8引擎做个简单的介绍。

栈(Stack)

主要用于存放基本类型和变量类型的指针。栈内存自动分配大小相对固定的内存空间,并由系统自动释放。

堆(Heap)

主要用于存放对象类型数据,如对象,数组,函数等等。堆内存是动态分配内存,内存大小不一,也不会自动释放。

垃圾回收算法

为了更好的回收内存,JavaScript引擎中有一个垃圾回收器(gc),它的主要作用是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。所以问题的重点在于如何判断这个被占用的内存不再被使用,可以被释放掉。JS提供了一系列算法来帮助判断变量是否被引用。

一,引用计数

引用计数是最初的垃圾回收算法,它将“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收

其思路是,每个值都记录它被引用的次数。声明变量并给它赋值时,引用数为1,如果一个值又被赋值给另一个变量,那么引用次数加1。如果引用值被其他的值覆盖了,引用数减1。垃圾回收机制运行时,会回收内存中引用数为0的变量。示例:

var o = {
  a:{
    b:2
  }
};
//两个对象被创建,一个对象作为另一个的属性被引用,另一个对象被分配给变量o
//此时,这两个对象的引用数都为1,无法被回收。
//我们通过 ot = 1(o引用); at = 0; 来表示两个对象的引用次数. 
//at作为对象ot的属性,且ot还在被引用,暂时不能被回收

var o2 = o; //变量o2再次引用了对象,此时 ot = 2 (o,o2引用) ;at = 0 ;
o = 1;  //ot = 1 (o2引用) ;at = 0 ;

var oa = o2.a;  // ot = 1 (o2引用) ;at = 1 (oa引用);
o2 = 'ya';  // ot = 0 ;at = 1 (oa引用);
// 此时,虽然ot是零引用,但是它的属性a对象还被引用这,还无法被垃圾回收。
oa = null; // ot = 0;at = 0; 此时这两个对象都没有被引用,可以被垃圾回收了。

但该算法有一个限制,如果出现循环引用就无法回收了。

function problem(){
    let object1 = new Object();
    let object2 = new Object();

    object1.A = object2;
    object2.B = object1;   //object1,object2相互引用
}

优化:减少内存的消耗,全局对象的属性或变量不引用时将其值设置为null。

二,标记清除

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。它的原理是:在内存中跟踪每个对象的使用情况,并标记所有不再使用的对象,然后,已标记的对象都会被清除,以释放内存。

代码在执行时,变量的取值是从上下文中的取得。在代码的解释阶段,当声明一个变量时,这个变量会加上一个存在于上下文中的标记。垃圾回收机制运行的时候,会标记内存中存储的所有变量,然后会将所有上下文中存在的变量的标记去掉(上下文中的变量都是代码在后续执行过程中会用到的变量),最后有标记的变量就是待清除的变量。

V8引擎的垃圾回收机制

目前JavaScript最流行的引擎就是V8引擎,它的内存回收机制与传统机制相比又做了许多升级和优化。 v8提出了一个弱分代假说 ,它的垃圾回收机制主要是基于这个假说的。

假说基本思想是:绝大部分的对象生命周期都很短,生命周期很长的对象基本都是常驻对象

基于这这个假说 v8引擎将堆内存主要分为新生代老生代两个区域(还有一些其他的区域,但垃圾回收主要在这两个区域进行)。新生代主要存储生命周期比较短的对象,老生代则存储生命周期比较长的对象。这两个区域的垃圾回收机制也有所差别。

v8引擎的内存结构

深入JavaScript之内存管理 图片来源www.imooc.com/article/300…

v8引擎将堆内存(Heap memory)分为了五个部分:

  • 新生代(New space):大多数对象创建时一般都会分配到这块区域,这块垃圾回收较为频繁,经过一次回收依旧存活的对象会放入老生代中。
  • 老生代(Old space):新生代的对象存活一段时间就会放入老生代,老生代内存区域垃圾回收频率较低,存放的是生命周期较长的对象。
  • 大对象区(Large object space):存放体积超越其他区域大小的对象,垃圾回收不会移动大对象区域。
  • 代码区(Code space):这里是即时(JIT)编译器存储编译代码块的地方,即代码对象会被分配到这里,唯一拥有执行权限的内存区域。
  • Map区:主要包括单元空间(cell space),属性单元空间(Property cell space),映射空间(Map space)。这些空间中每一个都包含相等大小的对象。

新生代

新生代区域的划分

v8引擎将新生代划分为两个相等的半空间(Semi space):From spaceTo space同一时间只有一个半空间在工作,另一个半空间处于休闲状态。处于工作状态的半空间叫做 From space,处于休闲状态的半空间叫做To space。

深入JavaScript之内存管理

新生代垃圾回收算法

在新生代中,主要使用 Scavenge 算法进行垃圾回收,Scavenge是清除的意思,这是一种典型的以空间换时间的算法。

Scavenge算法的主要思路:代码执行时,程序中首先声明的对象会放到 From space 中,垃圾回收时,将 From space 中非存活对象直接清除存活对象复制到 To space 中, 然后将这些对象内存有序排列。之后,From space中的内存直接清空。最后,From space 和 To space 完成一次角色互换。To space 会变成新的 From space 空间,From space会变成新的 To sapce空间。

其活动流程如下图: 深入JavaScript之内存管理

对象是否存活的判断

这里就要说到一个概念:可达对象。在一个作用域链上,只要通过根可以有路径查找到的对象都是可达对象,也就是之前说的存活对象。在JavaScript中,根可以理解为全局变量对象,也就是window。

新生代对象的晋升

之前说过新生代存储声明周期较短的对象,老生代存储声明周期较长的对象。但代码执行时声明的对象都是放到新生代的 From space 中,所以一个对象在新生代中经过多次复制后还存在,下一次垃圾回收时会将其放入老生代中,这个过程称之为对象的晋升。新生代中对象的晋升有两种情况:

  1. 经过一次 Scavenge 算法(新生代的一次翻转置换过程)在新生代中还存在的对象。
  2. 在进行Scavenge 算法的复制过程时,如果To space 空间的占比超过25%,则直接将该对象放入老生代中。

25%的内存限制是因为 To space 在经历过一次Scavenge算法后会和 From space 完成角色互换,会变为From空间,后续的内存分配都是在From空间中进行的,如果内存使用过高甚至溢出,则会影响后续对象的分配,因此超过这个限制之后对象会被直接转移到老生代来进行管理。

老生代

老生代垃圾回收算法

和新生代不同,老生代采用的是标记清除(Mark Sweep)标记整理(Mark Compact) 算法进行垃圾回收。

标记清除算法思路:标记清除算法主要包含标记和清除两个阶段。标记阶段就是从一组根元素开始,遍历递归这组根元素,在这个过程中,能达到的对象就是活动对象,对它们做个标记。没有达到的对象可以判断为垃圾数据。然后是清除阶段,直接清除没有做标记的对象。

如图所示:深入JavaScript之内存管理

在进行一次标记清除算法后,内存空间可能会出现不连续的情况,也就是内存碎片化问题。如果分配一个大对象可能总内存剩余空间足够,但由于内存碎片化而无法存储。为了解决这个问题提出了标记整理算法,通过移动内存中的可达对象将碎片化内存变成一个整体。

标记整理算法思路:和标记清除类似,标记整理也是先给内存中的可达对象做标记,然后在将其整理排序。深入JavaScript之内存管理

参考文章