Table of Contents
第三章、异步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在处理完我们提供的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的辨析:
- setImmediate的回调是在check阶段执行,而process.nextTick是在每次将要进入一个新阶段时都会检查执行,所以这两个函数的名字其实取反了;
- 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的内存的,所以解决方法就是:
- 使用不属于堆内存的buffer,buffer不受这个堆内存大小限制,只和物理内存多少有关;
- 在启动node应用的时候把堆内存的大小限制参数改大;
- 写应用时节约内存的使用并且注意不要产生内存泄漏;
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
近期评论