Node内存由V8进行分配的部分和Node自行分配的部分。
V8的垃圾回收机制与内存限制
V8的内存限制
因为V8的垃圾回收限制,在node中使用内存:64位系统约1.4G,32位系统约0.7G;因此我们无法读入一个2G的文件读进内存中进行字符串分析处理。
默认情况下:
- 64位系统:
- 老生代内存的最大值是1400MB;
- 新生代内存的最大值是32MB;
- 32位系统:
- 老生代内存的最大值是700MB;
- 新生代内存的最大值是16MB;
V8为什么要限制堆的大小?
V8的垃圾回收机制的限制—垃圾回收中会引起js线程暂停执行,V8做一次垃圾回收时间花销较大(以1.5G的垃圾回收堆内存为例,一次小型的垃圾回收需要50ms以上,非增量式的垃圾回收需要1s以上),影响应用的性能和响应能力。
V8的对象分配
V8中是通过堆来对所有的js对象进行分配—当我们声明变量并赋值时,所用对象的内存就分配到堆中,若已申请的堆空间内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8的限制为止;
- new space(默认最大限制2M):新生代对象;
- old space(老生代):
- old data space:存放达到一定生存率的新生代对象;
- code space:存放编译出来的代码;
- map space:保存对象的map信息;
- large object space:存放大对象(new space放不下的对象);
通过process.memoryUsage()查看V8中内存使用情况:
heapTotal是已申请的堆内存,heapUsed是当前使用的量
V8的堆
V8的垃圾回收机制
scavenge算法:清除new space的内存,牺牲空间换取时间;
- 算法流程:将新生代内存分为相等的两份:(1)使用;(2)闲置;
- 当垃圾回收开始时,会检查From空间中的存活对象,将这些存活对象复制到To空间中,非存活的对象在这个过程中就被释放掉了;
- 复制完成后,To和From空间互换;
- 新生代晋升至老生代:
- 某个对象经历了一次Scanvenge回收,第二次回收时直接将其复制到old data space空间;
- To空间已经使用了超过了25%;
Mark-Sweep(标记清除)
- 标记阶段:遍历堆中的所有对象,并标记活着的对象;
- 清除阶段:只清除没有被标记的对象;
最大的问题:内存碎片—进行一次标记清除回收后,内存空间会出现不连续的状态:如果出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
Mark-Compact(标记整理)
- 标记阶段:遍历堆中的所有对象,并标记活着的对象;
- 整理阶段:将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。
最大的问题:耗时—需要移动对象,因此执行速度不可能很快。
高效使用内存
作用域(scope):函数调用、with、全局作用域
-
标识符查找
js在执行时会查找该变量定义在哪里,最先查找当前作用域,若当前作用域无法找到该变量的声明,将会向上级的作用域里查找,直到查找到为止。 -
作用域链
标识符的查找会一直沿着作用域链查找到全局作用域,最后抛出未定义错误—查找方向是向上的,因此变量只能向外访问,不能向内访问。 -
变量的主动释放
全局变量(不通过var声明或定义在global变量上)需要直到进程退出时才能释放,常驻在老生代中,主动释放的方式:(1)通过delete操作来删除;(2)将变量重新赋值为undefined/null;在接下来的老生代内存清除和整理阶段会被回收释放,以上也适合于非全局变量。
闭包
实现外部作用域访问内部作用域中变量的方法—利用中间函数叫闭包;
一旦有变量引用这个中间函数,这个中间函数将不会释放,同时会使原始的作用域不会释放,作用域中产生的内存占用不会得到释放。
内存泄漏
原因及后果
-
后果
- 造成堆积,垃圾回收过程会耗费更多时间进行对象扫描,应用响应缓慢;
- 进程内存溢出,应用崩溃;
-
原因:应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象:
- 缓存:eg:模块机制;
- 限制缓存的大小;
- 采用进程外的缓存:(1)减少常驻内存的对象数量,让垃圾回收更高效;(2)进程之间可以共享缓存,避免重复缓存;
- 队列消费不及时:队列拥塞导致内存泄漏;
- 监控队列的长度:当堆积产生队列拥塞时,报警或拒绝入队;
- 超时机制:异步任务加入队列时开始计时,超时就直接直接响应一个超时错误;
- 作用域未释放:全局变量和闭包—内存无法立即回收;
大内存应用
通过stream模块处理大文件—若不需要进行字符串层面的操作,则不需要借助V8来处理;进行纯粹的Buffer操作,就不会受到V8堆内存的限制。
eg:当我们无法通过fs.readFile()和fs.writeFile()直接进行大文件的操作时,可以改用fs.createReadStream()和fs.createWriteStream()通过流的方式操作,可读流还提供了pipe()管道方法—封装data事件和写入事件。










网友评论