前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入浅出NodeJS随记 (二)

深入浅出NodeJS随记 (二)

原创
作者头像
邱邱邱邱yf
发布2021-12-10 16:36:38
4180
发布2021-12-10 16:36:38
举报
文章被收录于专栏:邱邱邱邱yf的读书笔记

最近在研读书籍 深入浅出nodejs , 随手写下的一些笔记, 和大家分享~ 如有错误,欢迎指正~

内存控制

随着node的发展,js已经不再局限于浏览器端了。但是当长时间运行在服务器端时,内存控制就很重要了,要为海量用户服务,就得使一切资源高效利用。

V8的垃圾回收机制与内存控制

1. V8的内存限制

64位系统下约为1.4G, 32位约为0.7G。这样的限制将会导致Node无法直接操作大内存对象,例如无法将一个2G的文件读入内存进行字符串分析处理。造成这个问题的原因在于Node基于V8构建,所以Node使用js对象都是通过V8的方式来进行分配管理的。这个形式在浏览器端绰绰有余, 但是在node中就限制了开发者。以下先阐述V8在内存使用上的策略

2. V8的对象分配

js对象都是通过堆来分配的,当我们在代码中声明对象,所用对象的内存就分配在堆中。如果已经申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8限制。

V8限制堆的大小的深层原因是V8的垃圾回收机制的限制。(按照官方的说法,以1.5G的垃圾回收堆内存为例, V8做一次小的垃圾回收需要50ms以上,做一次费增量式的垃圾回收甚至要1s以上。)这是垃圾回收中引起js线程暂停执行的时间,是不太能够接受的。因此限制堆内存是一个比较好的选择。

3. V8的垃圾回收机制

多种垃圾回收算法。 因为在实际的应用中,对象的生存周期长短不一,不同的算法只能针对特定情况具有最好的效果。因此现代的垃圾回收算法中按独享的存活时间将内存的垃圾回收进行不同的分代(新生代,老生代)然后针对不同的分代使用更高效的算法。

V8的内存分代

主要分为新生代和老生代。新生代中的对象为存活时间较短的对象,老生代为存活时间较长或常驻的对象。

V8堆的整体大小就是新生代加上老生代所用的内存空间。(在node启动时其实是可以手动设置的)

64位系统上 新生代约为32M, 老生代为1400M

32为系统上新生代约为16M, 老生代为700M

Scavenge算法

新生代中对象使用的算法。 其实是一种采用复制的方式实现的垃圾回收算法。它将内存一分为二,每一部分空间称为semispace。在两个semispace钟只有一个处于使用中(From空间),另一个为闲置状态(To空间)。我们分配对象时先是在From中进行分配。当开始垃圾回收时会检查From空间中的存活对象,存活对象会被复制到To空间中(其实在这之前还有一个检查对象是否需要晋升的步骤),而非存活对象占据的空间就会被释放。完成复制后,两个空间角色发生互换。简而言之,就是把存活对象在两个semispace空间之间进行复制。

缺点是只能使用堆内存的一半。由于只复制存活对象,对于生命周期端的场景存活对象只有少数,所以在效率上有优异的表现。典型的牺牲空间换时间算法,适合新生代使用。当一个对象经过多次复制仍然存活时,会被认为是生命周期较长的对象,然后被移动到老生代采用新的算法,这个过程称为晋升。晋升的条件主要有两个,一个是对象是否经历过Scavenge回收,一个是To空间的内存占比超过限制(25%,因为之后这个To就变成了From,如果占比过高,会影响后续的内存分配)。

Mark-Sweep & Mark-Compact

对于老生代的如果还采用Scavenge算法存在两个问题:一个是存活对象较多,导致复制效率低下;另一个是浪费一半空间。因此财通了Mark-Sweep & Mark-Compact结合的方式进行垃圾回收。

Mark-Sweep就是标记清除的意思,它分为标记和清除两个阶段。

与Scavenge复制对象不同,Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中清除没有被标记的对象。死对象在老生代中只占少数,所以更为高效。

Mark-Sweep的最大问题是在进行一次标记清楚后会出现内存不连续的状态(内存碎片)。这对后续的内存分配会造成问题,因为很可能导致无法分配一个大对象,而提前触发垃圾回收,但是这次回收其实是不必要的。

为了解决内存碎片的问题,Mark-Compact就被提了出来。 Mark-Compact是标记整理的意思,建立在标记清除的基础上。差别在于 对象被标记死亡后,在整理的过程中会将活的对象向一端移动,移动完成后清理边界外的内存。缺点是 由于需要移动对象,执行速度会降低。

最终取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代晋升过来的对象进行分配时才采用Mark-Compact。

增量标记

为了避免出现js应用逻辑和垃圾回收器看到不一致的情况,垃圾回收时都需要把应用逻辑暂停下来,待执行完垃圾回收以后再恢复,这称为“全停顿”(stop-the-world)。在V8的分代式垃圾回收中,一次小的垃圾回收只收集新生代,由于新生代默认配置较小,且存活对象少,所以即使全停顿也影响很小。但是老生代造成的全停顿就比较可怕需要改善。

为了降低全堆垃圾回收带来的停顿时间,V8先从标记入手,将原本要一口气停顿完成的动作改为 增量标记(incremental marking),也就是拆分为许多小步,每做完一步就让js应用逻辑执行一会,交替进行直到标记阶段完成。经过此改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。后续还引入了延迟清理和增量式整理,不再赘述。

高效使用内存

  1. 变量的主动释放 如果变量为全局变量,必须等到进程退出才能释放,此时将导致对象常驻老生代,可以通过delete或者赋值为null来让就对象脱离引用,让他被垃圾回收机制回收。
  2. 少用闭包 var foo = function() { var bar = function() { var local = 'local var'; return functoin() { return local; } } var baz = bar() console.log(baz()); } ​ /* 一般而言,在bar()函数执行完毕以后,局部变量local将会随着作用域的销毁而被回收。但是注意这里的特点在于返回值是一个匿名函数,且这个函数中具备了访问local的条件。虽然在后续的执行中,在外部作用域还是无法直接访问local, 但是若要访问它,只要通过这个中间函数就可以了。这就是闭包,但是他的问题是,一旦有变量引用这个中间函数,这个中奖函数将不会释放,同时也会使原始的作用域不会得到释放,作用域产生的内存占用也不会得到释放。 */

堆外内存

通过process.memoryUsage()的结果可以看到,堆中的内存用量总是小于进程的常驻内存用量,这意味着Node中的内存使用并非都是通过V8分配的。我们将不通过V8的称为堆外内存。例如使用Buffer()申请内存就不通过V8分配(原因在于浏览器中操作字符串可满足绝大多数需求,但是在node中还需要处理网络流文件I/O流,操作字符串远远不能满足性能需求)

受垃圾回收机制限制的主要是V8的堆内存。

内存泄漏

  1. 缓存 一旦一个对象被当做缓存使用,意味着将会常驻老生代,储存的键越多长期存活的对象也越多,导致垃圾回收机制在扫描整理时,做无用功。 另外严格的缓存有完善的过期机制,但是普通对象没有。(可以加入策略来限制缓存无线增长的问题)要小心使用!可以使用第三方的,如LRU。 解决方案:使用类似Redis, Memcached等.
  2. 队列消费不及时 生产-消费模型 一旦消费速度低于生产速度,就会形成堆积
  3. 作用域未释放

大内存应用

在nodo中不可避免的还是会存在操作大文件的情景。Node提供了stream模块用于处理大文件。例如fs中的createReadStream和createWriteStream方法通过流的方式实现对大文件的操作。pipe方法可以帮助更简洁的编码。如果不需要字符串层面的操作,可以尝试使用Buffer操作,这不会受到V8堆内存的限制。

异步编程

难点

难点1 异常处理

异步I/O的实现主要包括两个阶段: 提交请求和处理结果。这两个阶段中间有事件循环调度,彼此不关联。异步方法通常在第一个阶段提交请求后立即返回,因此异常一般并不发生在这和阶段,try/catch的功效起不到作用(只能捕获当前这次事件循环内的异常,对callback执行时抛出的异常无能为力)。

Node在处理异常上形成了约定,将异常作为第一个实参返回,为空则是没有异常抛出。

在我们自行编写的异步方法也需要遵循这样的原则:必须执行调用者传入的回调,正确传异常给调用者。

难点2 函数嵌套过深

一般是异步操作存在依赖关系,比如遍历目录的操作

难点3 阻塞代码

如何阻塞代码实现其他语言中sleep的效果(非常不建议用while大循环去模拟类似情形)

解决方案(书本出版较早,当时es6还没推出)

  1. 事件发布/订阅模式
  2. Promise/Deferred模式
  3. 流程控制库

现在ES6中的Promise, async await可以用来解决, 不再赘述。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内存控制
    • V8的垃圾回收机制与内存控制
      • 1. V8的内存限制
      • 2. V8的对象分配
      • 3. V8的垃圾回收机制
    • 高效使用内存
      • 堆外内存
        • 内存泄漏
          • 大内存应用
          • 异步编程
            • 难点
              • 难点1 异常处理
              • 难点2 函数嵌套过深
              • 难点3 阻塞代码
            • 解决方案(书本出版较早,当时es6还没推出)
            相关产品与服务
            云数据库 Redis
            腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档