美文网首页
前端进阶全栈 - Node的内存管理

前端进阶全栈 - Node的内存管理

作者: stevekeol | 来源:发表于2020-01-05 03:13 被阅读0次

0.背景

  • 如网页应用,命令行工具等短时间执行的场景,随着进程的退出,内存会释放,几乎没有内存管理的必要。
  • 基于无阻塞,事件驱动的node服务,具有内存消耗低的优点,非常适合处理海量的网络请求。

在服务端,资源向来都是寸土寸金,要为海量用户服务,就要使一切资源都要高效循环利用。在海量请求和长时间运行下,内存控制尤为重要!

一.V8的内存限制(内存的事实现状)

Node中通过javascript使用内存并不能达到物理内存的上限,实际上使用的在64位系统下约1.4G,32位系统下约0.7G。

因为Node基于V8创建,V8有自己的方式分配和管理内存。

  • 表层原因:V8最初为浏览器而设计,不太可能遇到使用大量内存的场景;
  • 深层原因: V8的垃圾回收机制的限制(以“非增量式”的回收1.5G的堆内存需要javascript线程暂停要1秒以上)

二.V8的对象分配(何时会遇到内存限制的状况?)

V8的所有jacascript对象都是通过堆来进行分配的。

当我们在代码中声明变量并赋值时,所使用的对象的内存就分配在堆内存中。如果已经申请的堆内存不够分配新的对象,则继续申请堆内存,直到堆的大小超过V8的限制。

//查看进程的内存使用量
process.memoryUsage();
{ rss: 23236608, //常驻内存
  heapTotal: 9682944, //申请到的堆内存
  heapUsed: 5559752, //已使用的堆内存
  external: 10132
}
//查看系统的内存使用量
> os.totalmem();
8418570240
> os.freemem();
2272493568

当然,V8在Node启动时可通过传递参数,来突破该内存限制:

node --max-old-space-size=1700 index.js //1700MB
//or
node --max-new-space-size=1024 index.js //1400KB

在Node运行时,不能动态修改!

三.V8的垃圾回收机制(V8为何有内存限制?)

V8的垃圾回收策略主要是分代式垃圾回收机制。所谓分代,即将内存分为新生代和老生代两代。

  • 新生代中的对象:存活时间较短的对象;
  • 老生代中的对象:存活时间较长的对象或常驻内存的对象。

V8堆的大小 = 新生代内存空间 + 老生代内存空间

1.新生代的垃圾回收(Scavenge算法)

解析:

  • 新生代内存空间均分为二,每部分成为semispace。在某一时刻只有一个处于使用中(From空间),另一个处于闲置状态(To空间);
  • 分配对象时,先在From空间进行分配;
  • 开始垃圾回收时,检查From空间的存活对象并分配到To空间,非存活对象占用的空间被释放,完成复制后,From空间和To空间角色互换。

V8堆的大小 = 新生代内存空间(From空间 + To空间) + 老生代内存空间

评价:

  • 典型的牺牲空间换时间:仅仅使用了新生代堆内存空间的一半,(由于生命周期短的场景中存活对象只占少部分)因此复制时时间效率很高;
2.新生代对象向老生代对象的'晋升'

当一个对象复制多次依然存活时,将会被认为时生命周期长的对象。该对象随后(检查From空间时)会被移动到老生代中,晋升流程:

  • From空间中,该对象是否经历过Scavenge回收?是,进入老生代;否,进入To空间;
  • From空间中,判断To空间使用率是否超过25%?是,进入老生代;否,进入To空间;
3.老生代的垃圾回收(Mark-Sweep & Mark-Compact)

老生代中垃圾回收是将Mark-Sweep和Mark-Compact相结合。

Mark-Sweep(标记清除)算法解析:

  • 标记阶段:遍历堆中所有对象,并标记活着的对象;
  • 清除阶段: 只清除没有标记的对象。

评价:

  • 最大的问题是每次标记清除回收空间后,内存空间会出现不连续。这种内存碎片会对随后的内存分配造成问题,假如需要分配一个大对象,如果所有碎片空间都无法完成此次分配,则会提前触发不必要的垃圾回收。

Mark-Compact(标记整理)算法解析:

  • 标记阶段:编理堆中所有对象,标记死亡对象;
  • 整理阶段: 将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存;
4. 增量标记

'全停顿':以上垃圾回收的三种方式中,为了避免javascript逻辑和垃圾回收器看到的不一致,需要将javascript暂停下来,执行完垃圾回收后在恢复执行javascript逻辑。

为了降低该停顿时间,V8在标记阶段,将原本需要一口气停顿完成的动作改为'增量标记':拆分成许多小“步进”,每完成一个小步进,就让javascript逻辑执行一会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。

此外,在清除和整理阶段,V8也逐步引入增量式。

5. 查看垃圾回收的效率

(待)

四. 高效使用内存(实际编码中,如何让垃圾回收机制高效工作呢?)

在正常的javascript执行中,无法立即回收的内存有闭包和全局变量引用。因为这两种将导致老生代中的对象增多。

1.闭包

通常作用域链上的对象只能向上访问,外部无法访问内部。

  var local = '局部变量';
  (function() {
    console.log(local);
  }())
//正常打印
  (function() {
    var local = '局部变量';
  }())
  console.log(local);
//抛出异常

'闭包': javascript中,实现外部作用域访问内部作用域中变量的方法叫做闭包。

这得益于高阶函数的特性:可以作为参数或返回值。

  var bar = function() {
    var local = '局部变量';
    return function() {
      return local;
    }
  }

  var baz = bar();
  console.log(baz());

特点是:

  • bar()的返回值是一个匿名函数,该函数具备了访问local的条件;
  • 若要访问local,只要通过bar()这个中间函数稍作周转即可;

闭包对内存回收机制的影响:
一旦有变量引用这个中间函数,该中间函数将不会释放,同时也会是原始的作用域不会得到释放,作用域中产生的内存占用也得不到释放。除非不再引用,才会逐步释放。

2.作用域

如果变量是全局变量(不通过var声明或直接定义在global变量上),由于全局作用域需要直到进程退出才会释放,此时将导致引用的对象常驻内存(即常驻在老生代内存中)。

只有通过delete删除引用关系或将变量重新赋值,让旧的对象脱离引用关系,才能在接下来的老生代内存清除和整理的过程中,被回收释放。

global.foo = 'globalData';
console.log(global.foo);
delete global.foo;
//或者
global.foo = undefined/null;
console.log(global.foo); //此后即可垃圾回收

五. 堆外内存(引申)

通过process.memoryUsage()的结果可看出,堆中内存使用量总是小于进程的常驻内存量。

实际上,Node的内存,主要是通过V8进行分配的堆内存和Node自行分配的部分(即'堆外内存')。受V8垃圾回收限制的主要是V8的堆内存。

var useMem = function() {
  var size = 200 * 1024 * 1024;
  var buffer = new Buffer(size);
  for(var i = 0; i < size; i ++) {
    buffer[i] = 0;
  }
  return buffer;
}
//headTotal 3.86MB heapUsed 2.07MB rss 11.12MB
//...
//headTotal 5.85MB heapUsed 1.85MB rss 3012.91MB

以上可看出Buffer对象不同于其它对象,它不经过V8的内存分配机制,因此不会有堆内存的大小限制。也意味着利用堆内存可以突破内存限制的问题。


注:以上均是自己技术栈的整理,仅供备忘。如需交流:stevekeol(微信号)

相关文章

网友评论

      本文标题:前端进阶全栈 - Node的内存管理

      本文链接:https://www.haomeiwen.com/subject/tgkqactx.html