JavaScript中内存管理

1.作为一个前端为什么要关注内存

  • 防止页面占用内存过大,引起客户端卡顿,甚至无响应(用户体验,性能方面)

  • Node.js使用V8引擎,内存对于后端服务的性能至关重要,因为后端服务的持久性,后端更容易造成内存溢出

2.JS数据类型与JS内存机制

数据类型
  • 原始数据类型:字符串(String)、数字(Number)、布尔(Boolean)、空对象(Null)、未定义(Undefined)、Symbol(ES6新增数据类型表示独一无二的值)

BigInt(ES6新增一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了Number 能够表示的安全整数范围) ->栈内存 【按值访问】水桶型

  • 引用数据类型:对象(Object),如数组(Array)、对象({})、函数(function)、日期(Data)等 -> 堆内存

  • 内存空间:栈内存(stack)、堆内存(heap)

栈内存
  • 水桶型

  • 定义一个变量var a=10的过程:先存值10 -> 声明变量a=undefined -> 关联 a=10

  • 定义 var a,b,c; a在栈底,b,c在栈顶;出栈时,先进后出,顺序为 c先,b, 最后是a

堆内存
  • 定义一个对象变量 var obj1={a:10};

1.先是在堆内存里找个空间放个{a:10},这个键值对对应着一个16进制的值0x8049368

2.然后在【栈内存】生成一个obj1 = 0x8049368(1.步中的值)

  • 函数也是这样存储在堆内存中的,如调用一个fn它会通过那个16进制值拿到堆内存中的字符串,如果调用fn(),就会执行堆内存中的方法。

3.JavaScript的垃圾回收机制

垃圾回收

固定时间间隔周期性自动执行垃圾回收,释放内存

  • 优势:简化程序的内存管理代码,降低负担,减少内存泄露问题

  • 不足:意味着程序员无法掌控内存。JS没有暴露任何关于内存的API。我们无法强制执行垃圾回收,无法干预内存管理

引用计数(因为存在循环引用的bug:已弃用)
  • 引用类型的值赋值给变量+1,此变量赋值给另一变量再+1

  • 当这个变量赋值为其他值时,这个引用值-1

  • 当引用次数为0时,说明没有变量访问这个值了

  • 当垃圾收集器下次运行时,就会释放引用次数为0的值所占内存

var o1 = { a: 10 }; // { a: 10 } +1
o2 = o1; // { a: 10 } +1

//循环引用
function fn() {
    var obj1 = { a: 10 };
    var obj2 = { b: 10 };
    obj1.a = obj2;//obj1的a属性指向obj2
    obj2.b = obj1;//obj2的b属性指向obj1
}

标记清除(当前主流浏览器,只是回收器的间隔不同)

变量进入环境时,这个变量标记为“进入环境”,而当变量离开环境时,标记为“离开环境”,最后垃圾回收器完成内存清除工作,销毁并释放标记为“离开环境”的值所占内存空间

执行环境:就是变量和函数可被访问的环境区域,就是它们的生存空间;每个执行环境都有一个变量对象(variable object)

全局执行环境:最外围的执行环境;web浏览器中变量对象是windows,Node中是Global对象

局部执行环境:函数的执行环境就是局部执行环境,执行开始->函数的环境进入环境栈->执行结束->推出环境栈,销毁变量对象,控制权返回之前的执行环境

function foo(){
    var a = 10; //被标记进入环境
    var b = 'hello'; //被标记进入环境
}
foo(); // 执行完毕,a,b被标记离开环境,内存被回收

4.V8内存管理机制

由于 V8 引擎的原因,Node 在操作大内存对象时受到了一些限制,在 64 位的机器上,默认最大操作的对象大小约为 1.4G,在 32 位的机器上,默认最大操作的对象大小约为 0.7G。

如果我们的 Node 程序会经常操作一些大内存的对象,可以对这个默认值进行修改:

node --max-old-space-size=1700 index.js
node --max-new-space-size=1024 index.js

其中,max-old-space-size 表示设置老生代内存空间的最大容量,max-new-space-size 表示这只新生代内存空间的最大容量。但这两个值也是有上限的,不能无限设置,其中老生代内存空间最大的值约为 1.7G,新生代最大内存空间约为 1.0G。

至于新生代和老生代,这是 V8 对内存的一个分类,后文再介绍。

回到操作大内存对象的问题,如果 1.7G 的内存还是不够大怎么办呢?要知道 1.7G 是在 V8 引擎层面上做出的限制,要想避开这种限制,我们可以使用 Buffer 对象,Buffer 对象的内存分配在 C++ 层面进行,不受 V8 引擎限制。

注:通过 process.memoryUsage() 方法可以用来查看 V8 引擎的内存使用量,通过 os.totalmem() 方法和 os.freemem() 方法分别可以查看操作系统的总内存和空闲内存。

// 查看 V8 的内存使用情况
process.memoryUsage()
{ 
  rss: 31469568,
  heapTotal: 7708672,
  heapUsed: 5152856,
  external: 8609 
}

// 查看操作系统总内存
os.totalmem()
8279511040
// 查看操作系统的空闲内存
os.freemem()
1610977280

通过上面几个方法获取到的内存都是以字节为单位。

新生代和老生代

V8 将内存分为两类:新生代内存空间和老生代内存空间,新生代内存空间主要用来存放存活时间较短的对象【64位下大概内存量为32MB】,老生代内存空间主要用来存放存活时间较长的对象。对于垃圾回收,新生代和老生代有各自不同的策略,下面依次进行介绍。

新生代垃圾回收

新生代内存中的垃圾回收主要通过 Scavenge 算法进行,具体实现时主要采用了 Cheney 算法。Cheney 将内存空间一分为二,每部分都叫做一个 Semispace,这两个 Semispace 一个处于使用,一个处于闲置。处于使用中的 Semispace 也叫作 From,处于闲置中的 Semispace 也叫作 To。

在垃圾回收运行时时,会检查 From 中的对象,当某个对象需要被回收时,将其留在 From 空间,剩下的对象移动到 To 空间,然后进行反转,将 From 空间和 To 空间互换。进行垃圾回收时,会将 To 空间的内存进行释放,如下图所示:

简而言之,就是 From 空间中存放不需要被回收的对象,To 空间中存放需要被回收的对象,当垃圾回收运行时,将 To 空间中的对象全部进行回收。

新生代对象的晋升

前面说过,新生代内存空间用来存放存活时间较短的对象,老生代内存空间用来存放存活时间较长的对象。新生代中的对象可以晋升到老生代中,具体有两种方式:

  1. 在垃圾回收的过程中,如果发现某个对象之前被清理过,那么会将其晋升到老生代内存空间中

  1. 在 From 空间和 To 空间进行反转的过程中,如果 To 空间中的使用量已经超过了 25%,那么就将 From 中的对象直接晋升到老生代内存空间中

老生代垃圾回收

说完新生代的垃圾回收,再来看下老生代中的垃圾回收。首先,老生代内存空间和新生代内存空间的结构不一样,其是一个连续的结构,而不像新生代内存空间那样分为 From 和 To 两个部分:

老生代内存空间中的垃圾回收有标记清除(Mark Sweep)和标记合并(Mark Compact)两种方式。

Mark Sweep(标记清除)

Mark Sweep 是将需要被回收的对象进行标记,在垃圾回收运行时直接释放相应的地址空间,如下图所示(红色的内存区域表示需要被回收的区域):

如上图所示,使用 Mark Sweep 进行垃圾回收会产生一个问题,就是垃圾回收后内存会出现不连续的情况,为了解决这个问题,出现了 Mark Compact 方案。

Mark Compact(标记合并)

Mark Compact 的思想有点像新生代垃圾回收时采取的 Cheney 算法:将存活的对象移动到一边,将需要被回收的对象移动到另一边,然后对需要被回收的对象区域进行整体的垃圾回收。

上图展示了在老生代内存空间使用 Mark Compact 进行垃圾回收的过程。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值