内存控制基于非阻塞、事件驱动的Node服务,具有内存消耗低的优势,非常适合处理海量网络请求。一、V8的垃圾回收机制和内存限制对于性能敏感的服务器程序,内存管理好不好,垃圾回收好不好,都会影响到服务。在Node中,这一切都与Node的JavaScript执行引擎V8息息相关。1.1Node和V8Node是一个建立在Chrome的JavaScriptruntime之上的平台。V8的性能优势使得用JavaScript编写高性能的后台服务程序成为可能。Node在JavaScript的执行上直接受益于V8,可以随着V8的升级享受更好的性能或新的语言特性,但也会受到V8的一些限制,比如内存限制。1.2V8的内存限制如果你在Node中使用JavaScript使用内存,你会发现只能使用部分内存(64位约1.4GB,32位约0.7G)。V8的内存限制会阻止Node直接操作大内存对象。出现这个问题的主要原因是Node是基于V8构建的,所以Node中使用的JavaScript对象基本上都是通过V8自己的方法来分配和管理的。1.3V8对象分配在V8中,所有的JavaScript对象都是通过堆分配的。Node提供了一种在V8中查看内存使用情况的方法。$node>process.memoryUsage(){rss:22044672,heapTotal:9682944,heapUsed:5296232,//Appliedheapmemoryexternal:8860}//代码中声明和赋值变量时使用的堆内存Objectmemoryis分配在堆上。如果分配的堆空闲内存不足以分配新的对象,它会继续申请堆内存,直到堆的大小超过V8的限制。为什么V8会限制堆的大小?表面原因是V8本来就是为浏览器设计的,不太可能遇到大内存的场景;深层原因是V8垃圾回收机制的局限性;开启使用更多内存的限制:node--max-old-space-size=1700test.js//单位是MB//或者node--max-new-space-size=1024test.js//单位是KB1.4V8的垃圾回收机制V8使用的各种垃圾回收算法:V8的主要垃圾回收算法V8的垃圾回收策略主要基于分代垃圾回收机制。在现代的垃圾回收算法中,内存的垃圾回收是根据对象的存活时间分为不同的代,然后对不同代的内存进行更高效的算法。V8的内存分代在V8中,内存主要分为两代:新生代和老年代。新生代的对象存活时间更短;老年代的对象存活时间较长或驻留在内存中的对象;V8堆的整体大小是新生代使用的内存加上老年代的内存空间。Scavenge算法是基于分代的,新生代中的对象主要通过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要使用了Cheney算法。切尼算法是一种通过复制实现的垃圾回收算法。在垃圾回收过程中,是通过复制两个半空间中存活的对象。Scavenge的缺点:只能使用一半的堆内存。Scavenge是典型的牺牲空间换取时间的算法,因此不能大规模应用于所有垃圾回收。当一个对象在多次复制中幸存下来时,它被认为是一个长期存在的对象。生命周期较长的对象将被移至老年代并使用新算法进行管理。将对象从年轻代移动到老年代的过程称为提升。Mark-Sweep&Mark-CompactMark-Sweep是标记去除的意思,分为标记和清除两个阶段。Mark-Sweep最大的问题是,一次mark-sweep恢复后,内存空间会出现不连续。Mark-Compact是markfinishing的意思,是在Mark-Sweep的基础上发展而来的;IncrementalMarking,为了避免JavaScript应用逻辑与垃圾回收器不一致,需要应用垃圾回收的三种基本算法逻辑暂停,垃圾回收完成后恢复应用逻辑的执行.这种行为称为“停止世界”。为了减少全堆垃圾回收带来的停顿时间,V8从标记阶段入手,将原本应该一口气完成的动作改为增量标记(IncrementalMarking)。1.5查看垃圾收集日志查看垃圾收集日志的主要方式是在启动时添加--trace_gc参数。当进行垃圾收集时,垃圾收集日志信息将被打印到标准输出。通过分析垃圾收集日志,可以了解垃圾收集的运行情况,找出垃圾收集的哪些阶段比较耗时,是什么触发了这些阶段。2.内存的高效使用在V8面前,开发者的责任就是如何让垃圾回收机制更高效的工作。2.1作用域在JavaScript中,有函数调用,有和全局作用域可以组成一个作用域。标识符查找作用域链变量的主动释放如果需要释放常驻内存中的对象,可以通过delete操作删除引用关系。或者重新赋值变量,让旧对象脱离引用关系。虽然删除操作和重新赋值的效果是一样的,但是在V8中通过delete删除对象的属性可能会干扰V8的优化,所以还是通过赋值来解引用比较好。2.2闭包在JavaScript中,从外部作用域访问内部作用域变量的方法称为闭包。这是由于高阶函数的特点:函数可以作为参数或返回值。闭包是JavaScript的一个高级特性,它可以用来产生许多巧妙的效果。一旦一个变量引用了这个中间函数,这个中间函数就不会被释放,原来的作用域也不会被释放,作用域内占用的内存也不会被释放。除非不再引用,否则将逐步发布。3.内存指标3.1查看内存使用情况查看进程内存使用情况调用process.memoryUsage()查看Node进程的内存使用情况。RSS是residentsetsize的缩写,是进程的常驻内存部分。进程的内存中有几部分,一部分是rss,其余部分在交换区(swap)或者文件系统(filesystem)。查看系统的内存使用情况OS模块中的totalmem()和freemem()用于查看操作系统的内存使用情况。分别返回系统的总内存和空闲内存,以字节为单位。$node>os.totalmem()8446971904>os.freemem()2469531648>3.2堆外内存堆中使用的内存量总是小于进程常驻内存量,也就是说进程中的内存使用量Node并不是全部通过V8分布式的。没有被V8分配的内存称为堆外内存。堆外内存可以突破内存限制。4.内存泄漏Node对内存泄漏非常敏感。线上应用一旦上万流量,哪怕是一个字节的内存泄漏都会造成堆积,垃圾回收过程扫描对象的时间也会变长。响应很慢,直到进程内存溢出,应用程序崩溃。内存泄漏的原因:缓存;队列消费不及时;范围未发布;4.1谨慎使用内存作为缓存cache在应用中有着重要的作用,可以非常有效的节省资源。缓存访问效率高于I/O效率。一旦缓存命中,I/O时间可以节省一次。一旦一个对象被用作缓存,就意味着它会常驻在老年代。缓存中存储的key越多,存活时间越长的对象就会越多,这会导致垃圾回收在扫描和整理时对这些对象做无用的工作。JavaScript开发者通常喜欢使用对象的键值对来缓存东西,但这与严格意义上的缓存不同。严格意义上的缓存有完整的过期策略,而普通对象的键值对则没有。一种无意识导致的内存泄漏场景:memoizememoize的原理是将参数作为key进行缓存,用内存空间换取CPU执行时间。缓存限制策略为了解决缓存中的对象永远无法释放的问题,需要增加一个策略来限制缓存的无限增长。缓存方案如何大量使用缓存,目前比较好的方案是使用进程外缓存,进程本身不存储状态。外部缓存软件有很好的缓存过期淘汰策略和自己的内存管理,不影响Node进程的性能。Node中主要解决了两个问题:将缓存转移到外部,减少驻留在内存中的对象数量,让垃圾回收更有效率;缓存可以在进程之间共享;目前市面上比较好的缓存有Redis和Memcached。4.2关注队列状态在大多数应用场景中,消费的速度远远快于生产的速度,不容易发生内存泄漏。但是一旦消费率低于生产率,就会形成积累。表面上的解决方案是切换到消耗率更高的技术。一个深度的解决方案是监控队列的长度。五、内存泄漏排查1.node-heapunp2.node-memwatch6。大内存应用Node提供了一个stream模块来处理大文件。
