JavaScript开发者很少在开发过程中遇到需要对内存进行控制的场景,也缺乏控制的手段。随着Node的发展,JavaScript的应用场景已经不局限于浏览器中,寸土寸金的服务器端要实现为海量用户服务,就得使一切资源要高效利用。
V8的内存限制 JavaScript与Java一样通过垃圾回收机制来进行自动内存管理,这使开发者不必时刻关注内存分配和释放的问题。
V8的内存分配 在一般的后端开发语言中,系统对内存使用基本没什么限制,然而Node只能使用部分内存(64位系统约1.4GB,32位系统约为0.7GB)。在这样的限制下,Node无法直接操作大内存对象。这个问题主要原因是Node基于V8构建,V8这套内存管理机制在浏览器上绰绰有余,但是在Node上,却限制了开发者使用大内存的想法。
高效使用内存 在V8,开发者所要具备的责任是如何让垃圾回收机制更加高效工作。
作用域 JavaScript能形成作用域的有函数调用、with、全局作用域:
1 2 3 var foo = function ( ) { var local = {} }
foo函数在每次调用会创建对应的作用域,函数执行结束后,该作用域将会销毁。同时作用域中声明的局部变量分配在该作用域上,随作用域的销毁而销毁。在作用域被释放后,局部变量失效,其对象会在下次垃圾回收时被释放。
标识符查找 所谓标识符,可以理解为变量名,执行下面函数时,会遇到local变量:
1 2 3 var foo = function ( ) { console .log (local) }
JavaScript在执行时回查找该变量定义在哪里,它最先查找的是当前作用域,如果在当前作用域无法找到该变量的声明,将会向上级作用域里查找,直到查到为止。
作用域链 1 2 3 4 5 6 7 8 9 10 11 12 13 var foo = function ( ) { var local='local val' var bar=function ( ){ var local='another val' var baz=function ( ){ console .log (local) } baz () } bar () } foo ()
当我们在baz函数中访问local变量时,由于作用域中的列表没有local,所以会向上一个作用域查找,接着会在bar函数执行得到的变量列表中找到local的定义,于是使用它。尽管在更上一层的作用域中也存在local的定义,但是不会继续查找了。
变量的主动释放 如果变量时全局变量,由于全局作用域要直到进程退出才能释放,此时会导致引用的对象常驻内存。如果需要释放常驻内存的对象,可以通过delete来释放。
1 2 3 global .foo ="I am global object" console .log (global .foo )delete global .foo
闭包 作用域链上的对象访问只能向上,这样外部无法访问内部:
1 2 3 4 5 6 7 8 var foo = function ( ) { (function ( ) { var local = "local val" ; })() console .log (local); } foo ()
在JavaScript中,实现外部作用域访问内部作用域中变量的方法叫做闭包,这得益于高阶函数的特性:函数可以做为参数或者返回值:
1 2 3 4 5 6 7 8 9 10 11 var foo = function ( ) { var bar = function ( ) { var local = "local val" return function ( ) { return local; } } var baz = bar () console .log (baz ()) } foo ()
一般而言,在bar函数执行完后,局部变量local会随着作用域的销毁而被回收,但是这里的返回值是一个匿名函数,这个函数具备了访问local的条件,虽然在后续的执行中,外部作用域还是无法直接访问local,但是可以通过这个中间函数周转即可访问。
闭包是JavaScript的特性,它的问题在于,一旦有变量引用这个中间函数,这个中间函数不会被释放,同时也会使原始的作用域不会得到释放。
内存指标 查看内存使用的情况 调用process.memoryUsage()可以查看内存的使用情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var showMem = function ( ) { var mem = process.memoryUsage () var format = function (bytes ) { return (bytes / 1024 / 1024 ).toFixed (2 ) + 'MB' } for (const key in mem) { if (Object .hasOwnProperty .call (mem, key)) { const element = mem[key]; mem[key] = format (element) } } console .log (mem); } showMem ()
写一个方法用于不停地分配内存但不释放内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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 ${mem.rss} ` ) console .log ("--------------------------------------" ); } var useMen = ( ) => { 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 (let index = 0 ; index < 15 ; index++) { showMem () total.push (useMen ()) } showMem ()Process :heapTotal 4. 77MB,heapUsed 3. 99MB,rss 19. 44MB-------------------------------------- Process :heapTotal 164. 78MB,heapUsed 164. 04MB,rss 181. 47MB-------------------------------------- Process :heapTotal 325. 79MB,heapUsed 323. 87MB,rss 341. 90MB-------------------------------------- Process :heapTotal 488. 54MB,heapUsed 483. 89MB,rss 502. 35MB-------------------------------------- Process :heapTotal 652. 55MB,heapUsed 643. 85MB,rss 662. 57MB-------------------------------------- Process :heapTotal 820. 56MB,heapUsed 803. 85MB,rss 823. 16MB-------------------------------------- Process :heapTotal 996. 57MB,heapUsed 963. 85MB,rss 983. 77MB-------------------------------------- Process :heapTotal 1156. 07MB,heapUsed 1123. 36MB,rss 1143. 75MB-------------------------------------- Process :heapTotal 1316. 08MB,heapUsed 1283. 43MB,rss 1303. 76MB-------------------------------------- Process :heapTotal 1476. 09MB,heapUsed 1443. 64MB,rss 1463. 77MB-------------------------------------- Process :heapTotal 1636. 10MB,heapUsed 1603. 64MB,rss 1623. 78MB-------------------------------------- Process :heapTotal 1796. 11MB,heapUsed 1763. 64MB,rss 1783. 84MB-------------------------------------- Process :heapTotal 1956. 11MB,heapUsed 1923. 64MB,rss 1943. 85MB-------------------------------------- Process :heapTotal 2116. 12MB,heapUsed 2083. 36MB,rss 2103. 86MB-------------------------------------- FATAL ERROR : Reached heap limit Allocation failed - JavaScript heap out of memory
可以看到每次调用useMem都导致了三个值的增长,循环只执行了14次,在2000MB左右的时候,无法继续分配内存,进程内存溢出。
堆外内存 将前面的useMem中的Array改成Buffer,然后再次运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 var useMen = ( ) => { var size = 20 * 1024 * 1024 var arr = new Buffer (size) for (var i = 0 ; i < size; i++) { arr[i] = 0 ; } return arr; } Process :heapTotal 4. 77MB,heapUsed 3. 99MB,rss 19. 43MB-------------------------------------- Process :heapTotal 5. 27MB,heapUsed 4. 56MB,rss 41. 88MB-------------------------------------- Process :heapTotal 5. 27MB,heapUsed 4. 57MB,rss 61. 89MB-------------------------------------- Process :heapTotal 6. 27MB,heapUsed 4. 40MB,rss 82. 41MB-------------------------------------- Process :heapTotal 5. 77MB,heapUsed 4. 40MB,rss 102. 50MB-------------------------------------- Process :heapTotal 5. 77MB,heapUsed 4. 40MB,rss 122. 55MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 50MB,rss 142. 59MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 88MB,rss 162. 61MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 89MB,rss 182. 61MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 88MB,rss 202. 64MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 88MB,rss 222. 69MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 88MB,rss 242. 69MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 51MB,rss 262. 70MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 51MB,rss 282. 70MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 88MB,rss 302. 71MB-------------------------------------- Process :heapTotal 6. 77MB,heapUsed 3. 88MB,rss 322. 71MB--------------------------------------
可以看到15次循环都完整运行,并且heapTotal和heapUsed变化极小,唯一变化的是rss值。原因是Buffer对象不同于其他对象,它不经V8的内存分配机制,所以也不会有内存的大小限制。
从上面的例子可以得知Node的内存构成主要通过V8进行分配的部分和Node自行分配的部分,受V8的垃圾回收限制的是V8的堆内存。
内存泄露 V8的垃圾回收机制下,在通常的代码编写中,很少出现内存泄露的情况。但是内存泄漏通常产生于无意中,难以排查。通常,造成内存泄漏的原因有如下几个:
缓存
队列消费不及时
作用域未释放
慎将内存当缓存 JavaScript开发者通常喜欢用键值对来缓存东西,但这与严格意义上的缓存又有着区别,严格意义的缓存有着完善的过期策略,而键值对并没有。
下面代码利用对象十分容易创建一个缓存对象,但是受垃圾回收机制的影响,只能小量使用:
1 2 3 4 5 6 7 8 9 10 11 var cache = {}var get = (key ) => { if (cache[key]) { return cache[key] } else { } } var set = (key, value ) => { cache[key] = value }
所以在Node中,任何试图拿内存当缓存的行为都应该被限制,小心使用。
缓存限制策略 为了解决缓存中的对象永远无法释放的问题,需要加入一种策略来限制缓存的无限增长。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var LimitTableMap = function (limit ) { this .limit = this .limit || 10 this .map = {} this .keys = [] } var hasOwnProperty = Object .prototype .hasOwnProperty LimitTableMap .prototype .set = function (key, value ) { var map = this .map var keys = this .keys if (hasOwnProperty.call (map, key)) { if (keys.length === this .limit ) { var firstKey = keys.shift (); delete map[firstKey] } keys.push (key) } keys[key] = value } LimitTableMap .prototype .get = function (key ) { return this .map [key] }
实现过程还是比较简单的,当然这种策略并不是十分高效,只能应付小型应用场景。
缓存的解决方案 直接拿内存做为缓存要十分慎重,除了限制缓存大小外,还要考虑到进程间无法共享内存。如何使用缓存,比较好的方法是采用进程外的缓存,如Redis和Memcached。
大内存应用 在Node中,不可避免地会出现操作大文件地场景,由于Node的内存限制,操作大文件也需要小心,Node提供了Stream用于处理大文件:
1 2 3 4 5 6 7 8 9 var fs = require ("fs" )var reader = fs.createReadStream ('in.txt' )var writer = fs.createWriteStream ('out.txt' )reader.on ('data' , function (chunk ) { writer.write (chunk) }) reader.on ('end' ,function ( ){ writer.end () })
Node可读流提供了管道pipe方法,封装了data事件和写入操作:
1 2 3 4 var fs = require ("fs" )var reader = fs.createReadStream ('in.txt' )var writer = fs.createWriteStream ('out.txt' )reader.pipe (writer)
虽然这时代码不会受到V8的内存限制,但是依然要小心,物理内存依然有限制。