计算机 · 2021年12月18日 0

深入理解NodeJS笔记

第三章、异步I/O

异步I/O之外的几个异步API:

设置定时器:

  • setImmediate(callback[,...args])
    在该次Nodejs事件循环的末尾执行这个函数
  • setTimeout(callback,delay[,...args])
    设置delay毫秒之后执行这个函数,一次性的
  • setInterval(callback,delay[,...args])
    按照delay毫秒的间隔执行这个函数

取消定时器:

  • clearImmediate(immediate)
  • clearInterval(timeout)
  • clearTimeout(timeout)

NodeJS的事件循环的解释

NodeJS在处理完我们提供的JS程序之后(我们提供的JS程序会设置timers,进行异步API调用,或者调用process.nextTick()函数等),便进入下面的事件循环(event loop):

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

上图中的每个框都是event loop里定义好的一个阶段phase,每进入一个新的阶段时,NodeJS先做一些特属于该阶段的事情,然后就开始执行该阶段的回调。当该阶段的回调执行完或者执行的回调数到达阈值后NodeJS就继续进入下一个阶段。
在event loop之间NodeJS会检查是否还有Timer需要执行或者是否有异步调用没有完成,如果没有,那么NodeJS就退出,整个NodeJS程序结束。

对于各个phase的解释:

  • timers
    执行setTimeout和setInterval函数设置的回调
  • pending callbacks
    执行被延迟到下一个循环执行的I/O回调。This phase executes callbacks for some system operations such as types of TCP errors. For example if a TCP socket receives ECONNREFUSED when attempting to connect, some *nix systems want to wait to report the error. This will be queued to execute in the pending callbacks phase.
    比较绕,意思是一些特殊的比如TCP错误事件的回调会在pending callbacks阶段执行呗。
  • idle,prepare
    NodeJS内部使用
  • poll
    读取I/O事件,执行I/O相关的回调(不包含close相关的回调,timers相关的回调,和setImmediate的回调)
  • check
    执行setImmediate设置的回调
  • close callbacks
    执行close相关的回调

关于setTimeout(fn, 0)和setImmediate,process.nextTick的辨析:

  1. setImmediate的回调是在check阶段执行,而process.nextTick是在每次将要进入一个新阶段时都会检查执行,所以这两个函数的名字其实取反了
  2. setTimeout(fn, 0)和setImmediate执行的先后顺序问题:
  // timeout_vs_immediate.js
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  
  setImmediate(() => {
    console.log('immediate');
  });

上面这段代码setTimeout和setImmediate的回调的执行先后顺序是不确定的。主要原因是setTimeout(fn, 0)其实是setTimeout(fn, 1),setTimeout,setInterval的delay最小是1ms,那么在进入事件循环的时候,由于最先进入的是timers这个阶段,首先检查setTimeout设置的这个1ms的timer回调,这个时候NodeJS内部的时间计数可能达到了1ms,也可能没有达到,如果达到了1ms那么就执行这个timer,如果没有达到1ms那么就会按照事件循环各个阶段顺序先执行check阶段里的那个setImmediate设置的timer,然后在第二次事件循环的timer阶段再来执行这个1ms的timer。具体可以看segmentfault的这篇讨论还有这篇文章中关于这个例子的解释

第五章、内存控制

1.V8的内存限制与垃圾回收算法

V8虚拟机当初设计是用在浏览器里面的,并且chrome的每个标签都有一个V8虚拟机实例,所以V8对堆内存做了一个只能使用1.4、1.5GB的限制,这样来限制每次垃圾回收的时间不会过长以致影响用户体验。但是我们写服务器是不可能忍的了只能用1、2个GB的内存的,所以解决方法就是:

  1. 使用不属于堆内存的buffer,buffer不受这个堆内存大小限制,只和物理内存多少有关;
  2. 在启动node应用的时候把堆内存的大小限制参数改大;
  3. 写应用时节约内存的使用并且注意不要产生内存泄漏;

v8的垃圾回收机制
将占用内存的对象分为两代,新生代和老生代。新生代指存活周期短的对象,老生代指存活周期长的对象。node分别用--max-new-space-size--max-old-space-size两个参数来限制这两种对象所占用的最大堆内存。按照书中的数据,老生代默认多达1.4GB,新生代只有16MB,32MB的样子。新生代采用Scavenge算法,具体的实现又采用了Cheney算法,其实就是内存分配器的伙伴系统,把新生代预先分配好的内存空间分两半,导来导去。因为新生代是声明周期短的对象,所以每次从一半空间拷贝到另一半空间时,只需拷贝活的对象就好了。老生代则采用Mark-Sweep的算法,把不再需要的对象占用的空间释放就好,为了避免产生过多的内存碎片,或者是有对象放不下的时候就跑一下Mark-Compact算法,就是每次先把不需要的对象移到内存空间的一端,然后再将其空间释放,这样就不会产生内存空间上的漏洞。新生代晋升为老生代的条件:该对象已经拷贝过一次了(即经历过一次Scavenge算法)或者To空间的内存占用比例超过限制(伙伴系统把内存分成了大小相等的From,To两个半空间,每次导完活着的对象后,From,To的角色互换)。当然全停顿的垃圾回收方式是很影响体验的,所以V8还有Incremental Marking,lazy sweeping和incremental compaction等设计,把垃圾回收的工作拆解优化,尽量不因为垃圾回收产生大的停顿。

几个用于分析垃圾回收的选项:

  • --trace_gc
  • --prof

2.高效使用内存

  • 使用delete来释放全局变量
  • 小心闭包

3.内存问题的分析

  • 查看进程内存占用
    process.memoryUsage()
  • 查看系统的内存占用
    os.totalmem()
    os.freemem()
  • 内存泄漏排查工具
    • node-heapdump
    • node-memwatch

第七章、网络编程

几种应用层协议:

  • HTTP
  • HTTPS
  • WebSocket