深入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的变量。
示例:

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引擎的内存结构

v8引擎堆内存结构.jpg
图片来源http://www.imooc.com/article/300729?block_id=tuijian_wz
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。
image.png

新生代垃圾回收算法

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

其活动流程如下图:
image.png

对象是否存活的判断

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

新生代对象的晋升

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

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

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

老生代

老生代垃圾回收算法

和新生代不同,老生代采用的是**标记清除(Mark Sweep)标记整理(Mark Compact)**算法进行垃圾回收。
标记清除算法思路:标记清除算法主要包含标记和清除两个阶段。标记阶段就是从一组根元素开始,遍历递归这组根元素,在这个过程中,能达到的对象就是活动对象,对它们做个标记。没有达到的对象可以判断为垃圾数据。然后是清除阶段,直接清除没有做标记的对象。
如图所示:
image.png
在进行一次标记清除算法后,内存空间可能会出现不连续的情况,也就是内存碎片化问题。如果分配一个大对象可能总内存剩余空间足够,但由于内存碎片化而无法存储。为了解决这个问题提出了标记整理算法,通过移动内存中的可达对象将碎片化内存变成一个整体。
标记整理算法思路:和标记清除类似,标记整理也是先给内存中的可达对象做标记,然后在将其整理排序。
image.png

参考文章

https://juejin.cn/post/6844904016325902344?searchId=202308281006199F433ADA43C9B473E7B4
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management
http://www.imooc.com/article/300729?block_id=tuijian_wz
https://juejin.cn/post/6995706341041897486?searchId=202308301111474A8B1CE6341C902FDDD9#comment

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值