深入JavaScript之内存管理

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的变量。

示例:

javascript 复制代码
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; 此时这两个对象都没有被引用,可以被垃圾回收了。

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

javascript 复制代码
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引擎的内存结构

图片来源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。

新生代垃圾回收算法

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

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

其活动流程如下图:

对象是否存活的判断

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

新生代对象的晋升

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

新生代中对象的晋升有两种情况:

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

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

老生代

老生代垃圾回收算法

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

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

如图所示:

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

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

参考文章

赠你13张图,助你20分钟打败了「V8垃圾回收机制」!!! - 掘金 (juejin.cn)
一文搞懂V8引擎的垃圾回收 - 掘金 (juejin.cn)
V8引擎详解(六)------内存结构 - 掘金 (juejin.cn)
V8引擎的内存管理_慕课手记 (imooc.com)

相关推荐
花生侠20 分钟前
记录:前端项目使用pnpm+husky(v9)+commitlint,提交代码格式化校验
前端
猿榜22 分钟前
魔改编译-永久解决selenium痕迹(二)
javascript·python
阿幸软件杂货间26 分钟前
阿幸课堂随机点名
android·开发语言·javascript
一涯28 分钟前
Cursor操作面板改为垂直
前端
我要让全世界知道我很低调35 分钟前
记一次 Vite 下的白屏优化
前端·css
threelab35 分钟前
three案例 Three.js波纹效果演示
开发语言·javascript·ecmascript
1undefined236 分钟前
element中的Table改造成虚拟列表,并封装成hooks
前端·javascript·vue.js
蓝倾1 小时前
淘宝批量获取商品SKU实战案例
前端·后端·api
comelong1 小时前
Docker容器启动postgres端口映射失败问题
前端