NodeJs 深入浅出之旅:内存控制(下)🐯
高效使用内存
前面说了那么多关于 v8 的垃圾回收机制,就是说明开发者需要让垃圾回收机制更加高效的使用
作用域
如何触发垃圾回收,第一个要介绍的就是作用域(scope)
。
作用域实际上就是变量和函数的
有效范围
,作用域控制着变量和函数的可见性
和生命周期
当前作用域分为:
全局作用域
函数作用域
块级作用域
作用域中声明的局部变量分配在该作用域上,随作用域销毁而销毁。在作用域释放后,局部变量引用的对象将会在下次垃圾回收时被释放。
全局作用域
该作用域下对象在任何地方都能访问,其生命周期伴随着页面的生命周期
以下内容都属于全局作用域
window 上的属性 (浏览器中)
window.location
最外层定义的函数
function add(){}
最外层定义的变量
var k = 5
未定义直接赋值的变量
a = 10
函数作用域
在函数内部定义的变量或者函数
函数在每次被调用时会创建其对应的作用域,函数执行结束后,该作用域会销毁。
作用域中声明的局部变量分配在该作用域上,随作用域销毁而销毁
定义的变量或者函数只能在函数内部被访问
例子:foo 函数中定义的 a 就在 foo 的函数作用域中
块级作用域
在 ES6 之前,是没有块的概念的。 在 ES6 之后,才出现了块级作用域。 同时新增了let
、const
等概念
凡是有代码块的地方就有块级作用域(由
{}
、()
等包裹,并且变量需要let
或者const
声明)块级作用域和函数作用域不是
块级作用域声明的变量不会提升到代码块顶部
例子:
下面两句
console
语句打印的结果是不相同的, 打印 a 输出undefined
, 打印 b 输出Uncaught ReferenceError: Cannot access 'b' before initialization
, 因为 let 定义的变量是块级作用域下
并且块级作用域内禁止重复声明。
例子:
作用域相关知识
标识符查找
所谓标识符,可以理解为变量名。
JavaScript 在执行时会去查找改变量定义在哪里,最先查找当前作用域,如果在当前作用域下无法找到该变量的声明,将会向上级的作用域里查找,直到查到为止。
作用域链
标识符查找时一层层往上找,这个查找过程形成的链条就是作用域链。
如果沿着作用域链查找到全局作用域都没有找到标识符相对应的变量声明,将会抛出未定义错误。
变量的主动释放
如果变量是全局变量,由于全局作用域需要直到进程退出才释放,将会导致引用的对象常驻内存(常驻在老生代中)。
在 JavaScript 中,可以使用delete
关键字手动删除变量,通过这样的方法让垃圾回收来回收内存
但是delete
关键字也具有局限性,不能删除使用var
、let
、const
定义的关键词
所以赋值方式还是通过解除引用来让 GC 自然回收更好。
内存指标
在之前《内存控制》和《V8 内存分配》中已经对内存分配进行过说明,关于如何查看内存使用情况和内存超出例子。
堆外内存
在process.momoryUsage
的结果可以看到,堆中内存用量总是小于进程的常驻内存用量,这意味着 Node 中的内存使用并非都是通过 V8 进行分配的,那些不是通过 V8 分配的内存称为堆外内存
内存泄漏
Node 对内存泄漏十分敏感,在 V8 垃圾回收机制下,通常的代码编写中,很少会出现内存泄漏的情况。内存泄漏通常产生于五一之间。
通常,造成内存泄漏的原因有如下几个:
缓存
队列消费不及时
作用域未释放
慎将内存当作缓存
缓存在应用中的作用举足轻重,可以十分有效地节约资源。 访问效率要比 I/O 的效率高,一旦命中缓存,就可以节省一次 I/O 的时间。
但在Node
中,缓存并非物美价廉。一旦一个对象被当作缓存来使用,那就意味着它将会常驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,导致 GC 进行扫描和整理是,对这些对象做无用功。
解决方案:
使用缓存限制策略,对缓存的数量进行限制
进程之间共享缓存
将缓存转移到外部,减少常驻内存的对象数量,让垃圾回收更高下
关注队列状态
在缓存带来的内存泄漏问题后,就是队列消费造成的内存泄漏问题了
队列在消费者-生产者模型中常常充当中间产物,但是在大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生。 但是一旦消费速度大于生产速度,就容易发生堆积。
解决方案:
监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员
任意异步调用都应该包含超时机制,一旦在限定时间内未产生响应,通过回调函数传递超时异常,给消费速度一个下限值。
大内存应用
在 Node 中,不可避免地存在操作大文件的场景,由于Node
的内存限制,操作大文件要小心。
不过Node
提供了stream
流模块用于处理大文件,这是继承自EventEmitter
,具备基本的自定义事件功能,同时抽象出标准的事件和方法。
流是为 Node.js 应用程序提供动力的基本概念之一。它们是一种以高效的方式处理读/写文件、网络通信、或任何类型的端到端的信息交换。
Node.js
的stream
模块 提供了构建所有流 API 的基础。 所有的流都是EventEmitter
的实例
stream
分为可读和可写两种,由于 V8 内存的限制,无法通过fs.readFile()
和fs.writeFile()
直接进行大文件操作,二十改用fs.createReadStream()
和fs.createWriteStream()
方法通过流的方式实现对大文件的操作。使用流,则可以逐个片段地读取并处理(而无需全部保存在内存中)
相对于其他数据处理方法,流基本上提供了两个主要优点:
内存效率:无需加载大量的数据到内存中即可进行存储
时间效率:当获得数据后即可立即开始处理数据,这样所需要的时间更少,而不必等到整个数据有效负载可用才开始。并且可以控制流的速度
例子:
版权声明: 本文为 InfoQ 作者【空城机】的原创文章。
原文链接:【http://xie.infoq.cn/article/7e1b7a4765ad913aa7e44d86a】。文章转载请联系作者。
评论