NodeJs深入浅出之旅:内存控制(下)🐯

举报
空城机 发表于 2021/11/28 15:53:16 2021/11/28
【摘要】 前面说了那么多关于v8的垃圾回收机制,就是说明开发者需要让垃圾回收机更加高效的使用,减少内存的泄漏,在对大内存应用时最好使用流的方式进行I/O操作

高效使用内存

前文《 内存控制(上)》《V8内存分配》

前面说了那么多关于v8的垃圾回收机制,就是说明开发者需要让垃圾回收机制更加高效的使用

作用域

如何触发垃圾回收,第一个要介绍的就是作用域(scope)

作用域实际上就是变量和函数的有效范围,作用域控制着变量和函数的可见性生命周期

当前作用域分为:

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域

作用域中声明的局部变量分配在该作用域上,随作用域销毁而销毁。在作用域释放后,局部变量引用的对象将会在下次垃圾回收时被释放。

全局作用域

该作用域下对象在任何地方都能访问,其生命周期伴随着页面的生命周期

以下内容都属于全局作用域

  • window上的属性 (浏览器中) window.location
  • 最外层定义的函数 function add(){}
  • 最外层定义的变量 var k = 5
  • 未定义直接赋值的变量 a = 10

函数作用域

在函数内部定义的变量或者函数

  • 函数在每次被调用时会创建其对应的作用域,函数执行结束后,该作用域会销毁。
  • 作用域中声明的局部变量分配在该作用域上,随作用域销毁而销毁
  • 定义的变量或者函数只能在函数内部被访问

例子:foo函数中定义的a就在foo的函数作用域中

var foo = function() {
  var a = 5;
}

块级作用域

在ES6之前,是没有块的概念的。 在ES6之后,才出现了块级作用域。 同时新增了letconst等概念

凡是有代码块的地方就有块级作用域(由{}()等包裹,并且变量需要let或者const声明)

块级作用域和函数作用域不是

块级作用域声明的变量不会提升到代码块顶部

例子:

下面两句console语句打印的结果是不相同的, 打印 a 输出undefined, 打印 b 输出 Uncaught ReferenceError: Cannot access 'b' before initialization, 因为let定义的变量是块级作用域下

(function test() {
    console.log(a);
    console.log(b);
    var a = 5;
    let b = 5;
})();

并且块级作用域内禁止重复声明。

例子:

(function test() {
    var a = 5;
    let a = 10;  // Uncaught SyntaxError: Identifier 'a' has already been declared
})();
(function test() {
    var a = 5;
    {
      let a = 10;  
    }
})();

作用域相关知识

  1. 标识符查找

所谓标识符,可以理解为变量名。

JavaScript在执行时会去查找改变量定义在哪里,最先查找当前作用域,如果在当前作用域下无法找到该变量的声明,将会向上级的作用域里查找,直到查到为止。

  1. 作用域链

标识符查找时一层层往上找,这个查找过程形成的链条就是作用域链。

如果沿着作用域链查找到全局作用域都没有找到标识符相对应的变量声明,将会抛出未定义错误。

  1. 变量的主动释放

如果变量是全局变量,由于全局作用域需要直到进程退出才释放,将会导致引用的对象常驻内存(常驻在老生代中)。

在JavaScript中,可以使用delete关键字手动删除变量,通过这样的方法让垃圾回收来回收内存

delete删除内存.png

但是delete关键字也具有局限性,不能删除使用varletconst定义的关键词

所以赋值方式还是通过解除引用来让GC自然回收更好。



内存指标

在之前《内存控制》和《V8内存分配》中已经对内存分配进行过说明,关于如何查看内存使用情况和内存超出例子。

堆外内存

process.momoryUsage的结果可以看到,堆中内存用量总是小于进程的常驻内存用量,这意味着Node中的内存使用并非都是通过V8进行分配的,那些不是通过V8分配的内存称为堆外内存

momoryUsage.png



内存泄漏

Node对内存泄漏十分敏感,在V8垃圾回收机制下,通常的代码编写中,很少会出现内存泄漏的情况。内存泄漏通常产生于五一之间。

通常,造成内存泄漏的原因有如下几个:

  • 缓存
  • 队列消费不及时
  • 作用域未释放

慎将内存当作缓存

缓存在应用中的作用举足轻重,可以十分有效地节约资源。 访问效率要比I/O的效率高,一旦命中缓存,就可以节省一次I/O的时间。

但在Node中,缓存并非物美价廉。一旦一个对象被当作缓存来使用,那就意味着它将会常驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,导致GC进行扫描和整理是,对这些对象做无用功。

解决方案:

  1. 使用缓存限制策略,对缓存的数量进行限制
  2. 进程之间共享缓存
  3. 将缓存转移到外部,减少常驻内存的对象数量,让垃圾回收更高下

关注队列状态

在缓存带来的内存泄漏问题后,就是队列消费造成的内存泄漏问题了

队列在消费者-生产者模型中常常充当中间产物,但是在大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生。 但是一旦消费速度大于生产速度,就容易发生堆积。

解决方案:

  1. 监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员
  2. 任意异步调用都应该包含超时机制,一旦在限定时间内未产生响应,通过回调函数传递超时异常,给消费速度一个下限值。


大内存应用

在Node中,不可避免地存在操作大文件的场景,由于Node的内存限制,操作大文件要小心。

不过Node提供了stream流模块用于处理大文件,这是继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。

流是为 Node.js 应用程序提供动力的基本概念之一。
它们是一种以高效的方式处理读/写文件、网络通信、或任何类型的端到端的信息交换。
Node.jsstream 模块 提供了构建所有流 API 的基础。 所有的流都是 EventEmitter 的实例

stream分为可读和可写两种,由于V8内存的限制,无法通过fs.readFile()fs.writeFile()直接进行大文件操作,二十改用fs.createReadStream()fs.createWriteStream()方法通过流的方式实现对大文件的操作。使用流,则可以逐个片段地读取并处理(而无需全部保存在内存中)

相对于其他数据处理方法,流基本上提供了两个主要优点:

  • 内存效率:无需加载大量的数据到内存中即可进行存储
  • 时间效率:当获得数据后即可立即开始处理数据,这样所需要的时间更少,而不必等到整个数据有效负载可用才开始。并且可以控制流的速度

例子:

const fs = require('fs');
// 创建流
let reader = fs.createReadStream('isolate-v8.log');
let writer = fs.createWriteStream('new-v8.log');

function save() {
    reader.on('data', (data)=>{
        writer.write(data);
    })
    reader.on('end', function() {
        writer.end();
    })
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。