Node——内存控制

JS在浏览器中运行的时候并不存在太大的内存问题,我们通常也不刻意的去优化他们,但是当运行在服务器端的时候,运行时间长,这种问题就不得不考虑了。

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

V8的内存限制

在64位下只能使用1.4GB,在32位下0.7GB。即便你的物理内存有32GB,单个Node进程也只能使用这些内存。如果你要将一个2G的文件读到内存里解析,good luck。
V8之所以要限制内存的大小,是因为V8垃圾回收的限制。以1.5G的垃圾回收堆内存为例,V8做一次小的垃圾回收要50毫秒以上,做一次非增量式的垃圾回收要1秒以上,这是在回收过程中JS线程被暂停的时间,这是不可接受的。所以目前比较好的办法就是限制住使用的内存。
这个限制也不是不能打破,你可以选择在启动时修改它:

node --max-old-space-size=1700 test.js // 单位为MB 
node --max-new-space-size=1024 test.js // 单位为KB 

这个只在初始化时生效,一旦生效就不能动态改变了。
新版本node的限制貌似取消了,至少我的机器到6个G时才报错的。

V8的垃圾回收机制

V8的垃圾回收机制主要基于分代式垃圾回收机制,将内存分为新生代和老生代两代,老生代是存活时间较长或常驻内存的对象,新生代为存活时间较短的。刚才那两个命令就分别是对这两个的设置。老生代的限制为1400/700MB,新生代的是32/16MB。
Scavenge算法
这个算法主要用在新生代内存区域中,因为这个算法的主要思想是牺牲空间来换取时间的。
算法将新生代内存分为相等的两份,一个使用,一个闲置。
处于使用状态的空间成为From空间,闲置的称为To空间,当我们分配对象时,是在From空间中进行分配的。
当垃圾回收开始时,会检查From空间中的存活对象,将这些存活对象复制到To空间中,非存活的对象在这个过程中就被释放掉了。复制完成后,To和From空间互换。
可以看到,它很快,但是费空间,不过对于新生代这种少量的内存来说是很划算的。
在单纯的Scavenge算法中,所有的存活对象都会被复制到To空间,但是在分代垃圾回收的大背景下,有些存活对象会被复制到老生代内存中。
当这个对象已经经历过一次Scavenge回收,它会被复制到老生代;当这个To空间已经使用了超过25%时,会被复制到老生代。因为To会在复制完成后变为From,新的内存分配在这里产生,它必须有足够的空余空间。
**Mark-Sweep & Mark-Compact **
在老生代中使用上面的算法显然是不可能的。
这里首先使用Mark-Sweep。这是标记清除法。它遍历堆中的所有对象,并标记活着的,在清除阶段中清除所有未被标记的对象。
在新生代中,只复制活的,在老生代中,只清理死的。这两个都分别是两部分中较少的那部分,所以这一整套垃圾回收比较高效。
在使用Mark-Sweep进行清除后,内存变得不连续了,这对接下来的内存分配会有影响,还会提前触发下一次垃圾回收。所以有了Mark-Compact,它将活着的对象往前移来填补空白。Mark-Compact过程是很慢的,V8只在空间不足分配新来的新生代时使用。
**Incremental Marking **
因为垃圾回收涉及对程序对象的删除,肯定需要将程序逻辑停下来,对于新生代来说不是什么问题,但是老生代就会很慢,于是有了增量标记,也就是垃圾回收与应用逻辑交替进行。
同样的还会有增量式整理和延迟清理。

高效使用内存

作用域

在某个局部作用域中的对象会随着局部作用域的销毁而被释放,在下次垃圾回收的时候就会清理掉这部分内存,如果全局作用域中的对象过多,那么这些对象存在的作用域直到继承退出才会被释放,这些对象也会最终停留在老生代内存区域中。
如果你想手动释放一个变量,可以使用delete操作符,但是并不推荐这样做,这样做会干扰V8引擎的优化,推荐使用将对象赋值为null或undefined来手动释放它。

闭包

闭包的使用使得JS有了许多优秀的特性,但是这样也带来了问题,一个闭包被赋值给一个变量以后,这个闭包所在的作用域也就不会被销毁,这个作用域中对象所使用的内存也不会被释放,这个要小心一下。

内存指标

进程的内存占用

使用process.memoryUsage()可以看到内存的使用情况。它返回的对象有3个属性rss:进程的常驻内存部分;,heapTotal是堆中总共申请的内存量;heapUsed表示目前堆中使用中的内存量。
我们可以测试一下:

 var showMem = function () {   
    var mem = process.memoryUsage();   
    var format = function (bytes) {     
        return (bytes / 1024 / 1024).toFixed(2) + ' MB';   
    };   
    console.log(
        'Process: heapTotal ' 
        + format(mem.heapTotal) 
        + ' heapUsed ' 
        + format(mem.heapUsed) 
        + ' rss ' 
        + format(mem.rss));   
    console.log('-----------------------------------------------------------'); 
 };
var useMem = function () {   
    var size = 20 * 1024 * 1024;   
    var arr = new Array(size);   
    for (var i = 0; i < size; i++) {     
        arr[i] = 0;   
    }   
    return arr; 
};  
var total = [];  
for (var j = 0; j < 150; j++) {   
    showMem();   
    total.push(useMem()); 
} 
showMem();

这个方法会不断的分配内存但不释放,到最后:

Process: heapTotal 6086.95 MB heapUsed 6083.24 MB rss 6099.39 MB ---------------------------------------------------------------- FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory 

这里可以看到,在所有的rss中,堆内存占了大部分。

系统内存的占用

使用os模块中的函数来查看机器的物理内存及其使用情况:

var os = require("os");
console.log(os.totalmem());
console.log(os.freemem());

堆外内存

从上面的结果中我们可以看到,堆内存的总量总是小于rss。
我们将前面的useMem方法稍微改造一下,每一次构造一个200M的对象:

var useMem = function () {   
    var size = 200 * 1024 * 1024;   
    var buffer = new Buffer(size);   
    for (var i = 0; i < size; i++) {     
        buffer[i] = 0;   
    }   
    return buffer; 
};
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 3012.91 MB 

可以看到,这里buffer并未被分派到堆内存中,Buffer对象不同于其他对象,它不经过V8的内存分配机制,所以也不会有堆内存的大小限制。
这意味着利用堆外内存可以突破内存限制的问题。

内存泄露

内存泄露在前端页面上问题不太大,但是在服务器端就是个不得不考虑的问题。造成这个问题的原因有:

  • 缓存
  • 队列消费不及时
  • 作用域未释放

慎将内存用作缓存

缓存是很有效的节省IO的办法,但是在Node中,一旦一个对象被当做缓存来使用的时候就要格外的小心了,这意味着它将常驻在老生代内存中,这样的缓存越大意味着垃圾回收在做越多的无用功。
所以创建一个有完善过期机制的缓存来控制缓存的增长是很有必要的。
可以通过限制键的数量等方法来控制缓存的增长。
还有一个通常会被我们忽略的问题,就是模块的缓存由于模块的缓存机制,它是常驻老生代的。我们通过exports导出的函数是可以访问文件模块中的私有变量的,这样每个文件模块在编译执行后形成的作用域由于模块缓存的原因不会被释放,所以设计模块时要十分小心内存泄露。这里举个例子:

var leakArray = []; 
exports.leak = function () {   
    leakArray.push("leak" + Math.random()); 
};

这里每次调用leak方法,都会导致局部变量leakArray不停的增加内存的占用。
且进程间无法共享内存,在进程内使用缓存会造成进程间缓存无法共享,这对内存是一种浪费。如果需要大量缓存,最好使用进程外缓存比如Redis和Memcached。

关注队列状态

这也是一个不经意产生的内存泄露。队列一般在消费者-生产者模型中充当中间人的角色,当消费大于生产时没有问题,但是当生产大于消费时,会产生堆积,就容易发生内存泄露。
比如收集日志,如果日志产生的速度大于文件写入的速度,就容易产生内存泄露,表层的解决办法是换用消费速度更高的技术,但是这不治本。根本的解决方案应该是监控队列的长度一旦堆积就报警或拒绝新的请求,还有一种是所有的异步调用都有超时回调,一旦达到时间调用未得到结果就报警。

内存泄露排查

node-heapdump
node-memwatch
这两个模块可以用来检测内存泄露,它们可以通过事件和抓取内存快照的方式来为我们分析哪里有内存泄露提供依据。

大内存应用

不可避免的我们会遇到大文件操作的问题。由于Node内存的限制,操作大内存时要小心。stream模块为我们提供了支持,这是一个原生模块。

var fs = require("fs");
var reader = fs.createReadStream('in.txt'); 
var writer = fs.createWriteStream('out.txt'); 
reader.on('data', function (chunk) {   
    writer.write(chunk); 
    console.log(chunk);
}); 
reader.on('end', function () {  
    writer.end(); 
}); 

由于读写模式固定,专门提供了一个pipe方法:

var reader = fs.createReadStream('in.txt'); 
var writer = fs.createWriteStream('out.txt'); 
reader.pipe(writer); 

如果并不是字符串层面的操作,则可以使用纯粹的Buffer来操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 垃圾回收机制 nodejs在执行JavaScript时,内存受到v8限制,64位约为1.4g,32位0.7g 所有...
    wmtcore阅读 7,713评论 0 7
  • 第一章 简介 J2SE平台的一大优势是它的自动化内存管理,避免了开发者去面对内存管理的复杂性。 本文以Sun J2...
    tianyiliusha阅读 883评论 0 1
  • 1 养狗前:下班后,累得要死,直接瘫软。 养狗后:下班后,累得要死,还得遛狗。 2 养狗前:独自享受薯条炸鸡。 养...
    Chloe静学姐阅读 1,656评论 0 0
  • 以前,他最喜欢说,我最近对什么都没耐心,我却对他说,我对你一直有耐心,其实,他不知道我有多喜欢他,多么的想和他一起...
    爱是缪斯阅读 411评论 0 0
  • 人刚出生的时候,世界对他来说是陌生的、未知的,而又充满了新鲜感。所以幼儿总是会用一双清澈懵懂的眼睛来看周遭的一切。...
    竹韵悠然阅读 490评论 0 2