Perkhidmatan Node dibina berdasarkan bukan sekatan dan dipacu peristiwa Ia mempunyai kelebihan penggunaan memori yang rendah dan sangat sesuai untuk mengendalikan permintaan rangkaian besar-besaran. Di bawah premis permintaan besar-besaran, isu yang berkaitan dengan "kawalan memori" perlu dipertimbangkan.
Js menggunakan mekanisme pengumpulan sampah untuk melaksanakan pengurusan memori automatik dan pembangun tidak perlu melakukannya. seperti bahasa lain ( c/c++), sentiasa memberi perhatian kepada peruntukan dan pelepasan memori semasa proses menulis kod. Dalam penyemak imbas, mekanisme kutipan sampah mempunyai sedikit kesan ke atas prestasi aplikasi, tetapi untuk program sisi pelayan yang sensitif prestasi, kualiti pengurusan memori dan kualiti kutipan sampah akan memberi kesan kepada perkhidmatan. [Tutorial berkaitan yang disyorkan: tutorial video nodejs, Pengajaran pengaturcaraan]
Node ialah masa jalan Js yang dibina pada Chrome Pada platform, V8 ialah enjin skrip Js Node
Dalam bahasa hujung belakang umum, tiada had penggunaan memori asas, tetapi dalam Node Apabila Js menggunakan memori, ia hanya boleh menggunakan sebahagian daripada ingatan. Di bawah sekatan sedemikian, Node tidak boleh mengendalikan objek memori besar secara langsung.
Sebab utama masalah ialah Node dibina pada V8, dan objek Js yang digunakan dalam Node pada dasarnya diperuntukkan dan diuruskan melalui kaedah V8 sendiri.
Dalam V8, semua objek Js diperuntukkan melalui timbunan. | dan yang terakhir ialah Jumlah yang digunakan pada masa ini.
Apabila pembolehubah diisytiharkan dan diberikan nilai dalam kod, memori objek yang digunakan diperuntukkan pada timbunan. Jika ingatan timbunan percuma yang digunakan tidak mencukupi untuk memperuntukkan objek baharu, ingatan timbunan akan terus digunakan sehingga saiz timbunan melebihi had V8
Mengapa V8 mengehadkan saiz timbunan Alasannya adalah bahawa V8 pada asalnya direka untuk pelayar Bagi reka bentuk, ia tidak mungkin menghadapi senario yang menggunakan sejumlah besar memori. Untuk halaman web, nilai had V8 adalah lebih daripada mencukupi. Sebab asas adalah had mekanisme kutipan sampah V8. Menurut kenyataan rasmi, mengambil 1.5G memori timbunan sampah sebagai contoh, v8 mengambil masa lebih daripada 50 milisaat untuk melakukan kutipan sampah kecil, malah mengambil masa lebih daripada 1 saat untuk melakukan kutipan sampah tanpa tambahan. Inilah masa semasa pengumpulan sampah yang menyebabkan utas JS menjeda pelaksanaan, dan prestasi dan tindak balas aplikasi akan merudum. Mengehadkan memori timbunan secara langsung adalah pilihan yang baik di bawah pertimbangan semasa.
Had ini boleh dilonggarkan Apabila Node dimulakan, anda boleh melepasiatau
untuk melaraskan saiz had memori Setelah ia berkuat kuasa, ia tidak boleh diubah secara dinamik. Seperti:1.4 mekanisme kutipan sampah V8
Pelbagai algoritma kutipan sampah yang digunakan dalam v8--max-old-space-size
--max-new-space-size
1.4.1 algoritma kutipan sampah utama V8
node --max-old-space-size=1700 test.js // 单位为MB // 或者 node --max-new-space-size=1024 test.js // 单位为KB
Saiz keseluruhan timbunan v8 = ruang memori yang digunakan oleh generasi baharu + ruang ingatan generasi lama
Nilai maksimum memori timbunan v8 hanya boleh menggunakan kira-kira 1.4GB memori di bawah Sistem 64-bit dan di bawah sistem 32-bit Hanya kira-kira 0.7GB memori boleh digunakan
Algoritma Scavenge
Berdasarkan generasi, objek dalam generasi baharu kebanyakannya sampah dikumpul melalui algoritma Scavenge. Pelaksanaan khusus Scavenge terutamanya menggunakan algoritma Cheney. Algoritma Cheney ialah algoritma pengumpulan sampah yang dilaksanakan dengan menyalin. Bahagikan memori timbunan kepada dua bahagian, dan setiap bahagian ruang dipanggil semiruang. Daripada dua separa ruang, hanya satu sedang digunakan dan satu lagi terbiar. Ruang separuh ruang yang digunakan dipanggil Dari angkasa, dan ruang dalam keadaan terbiar dipanggil Kepada angkasa. Apabila memperuntukkan objek, ia mula-mula diperuntukkan dalam ruang Daripada. Apabila pengumpulan sampah bermula, objek yang masih hidup dalam ruang Dari akan diperiksa Objek yang masih hidup ini akan disalin ke ruang Kepada dan ruang yang diduduki oleh objek yang tidak hidup akan dilepaskan. Selepas penyalinan selesai, peranan Dari angkasa dan Ke angkasa diterbalikkan. Ringkasnya, semasa proses pengumpulan sampah, objek yang masih hidup disalin antara dua separa ruang. Kelemahan Scavenge ialah ia hanya boleh menggunakan separuh daripada memori timbunan, yang ditentukan oleh ruang pembahagian dan mekanisme penyalinan. Walau bagaimanapun, Scavenge mempunyai prestasi cemerlang dalam kecekapan masa kerana ia hanya menyalin objek yang masih hidup, dan hanya sebahagian kecil daripada objek yang masih hidup digunakan dalam senario dengan kitaran hayat yang singkat. Memandangkan Scavenge ialah algoritma biasa yang mengorbankan ruang untuk masa, ia tidak boleh digunakan pada semua kutipan sampah secara besar-besaran. Tetapi Scavenge sangat sesuai untuk aplikasi dalam generasi baru, kerana kitaran hayat objek dalam generasi baru adalah pendek dan sesuai untuk algoritma ini. Memori timbunan sebenar yang digunakan ialah jumlah dua ruang separuh ruang dalam generasi baharu dan saiz memori yang digunakan dalam generasi lama.
Apabila objek masih bertahan selepas berbilang salinan, ia akan dianggap sebagai objek dengan kitaran hayat yang panjang dan akan dipindahkan ke generasi lama dan diuruskan menggunakan algoritma baharu. Proses memindahkan objek daripada generasi muda kepada generasi lama dipanggil promosi.
Dalam proses Scavenge yang mudah, objek yang masih hidup dalam ruang Dari akan disalin ke ruang Kepada, dan kemudian peranan ruang Dari dan ruang Kepada akan diterbalikkan (terbalik). Walau bagaimanapun, di bawah premis pengumpulan sampah generasi, objek yang masih hidup dalam ruang Dari perlu diperiksa sebelum disalin ke ruang Kepada. Dalam keadaan tertentu, objek dengan tempoh kemandirian yang panjang perlu dipindahkan ke generasi lama, iaitu promosi objek selesai.
Terdapat dua syarat utama untuk promosi objek Satu ialah sama ada objek telah mengalami kitar semula Scavenge, dan satu lagi ialah nisbah penggunaan memori ruang Kepada melebihi had.
Secara lalai, peruntukan objek V8 tertumpu terutamanya dalam ruang Daripada. Apabila objek disalin dari ruang Dari ruang ke ruang Kepada, alamat ingatannya akan diperiksa untuk menentukan sama ada objek itu telah mengalami kitar semula Scavenge. Jika ia telah dialami, objek akan disalin dari ruang Dari angkasa ke ruang generasi lama Jika tidak, ia akan disalin ke ruang Kepada. Carta aliran promosi adalah seperti berikut:
Satu lagi syarat penghakiman ialah nisbah penggunaan memori bagi ruang. Apabila menyalin objek dari ruang Dari ke ruang Kepada, jika ruang Kepada telah digunakan 25%, objek akan dinaikkan terus ke ruang generasi lama Carta aliran promosi adalah seperti berikut:
Sebab menetapkan had 25%: Apabila kitar semula Scavenge ini selesai, ruang Kepada ini akan menjadi Daripada angkasa, dan peruntukan memori seterusnya akan dijalankan dalam ruang ini. Jika nisbah terlalu tinggi, ia akan menjejaskan peruntukan memori seterusnya.
Selepas objek dipromosikan, ia akan dianggap sebagai objek dengan tempoh kemandirian yang lebih lama dalam ruang generasi lama dan akan diproses oleh algoritma kitar semula baharu.
Mark-Sweep & Mark-Compact
Untuk objek dalam generasi lama, memandangkan objek yang masih hidup menyumbang sebahagian besar, terdapat dua masalah jika anda menggunakan Scavenge: Satu ialah terdapat banyak objek yang masih hidup, dan kecekapan menyalin objek yang masih hidup akan menjadi sangat rendah; masalah lain ialah separuh daripada ruang itu terbuang. Untuk tujuan ini, v8 terutamanya menggunakan gabungan Mark-Sweep dan Mark-Compact untuk pengumpulan sampah pada generasi lama.
Mark-Sweep bermaksud tanda dan jelas, yang dibahagikan kepada dua peringkat: menanda dan mengosongkan. Berbanding dengan Scavenge, Mark-Sweep tidak membahagikan ruang memori kepada dua bahagian, jadi tidak ada tingkah laku membazir separuh daripada ruang. Tidak seperti Scavenge, yang menyalin objek hidup, Mark-Sweep merentasi semua objek dalam timbunan semasa fasa penandaan dan menandakan objek hidup Dalam fasa pembersihan berikutnya, hanya objek yang tidak bertanda dibersihkan. Ia boleh didapati bahawa Scavenge hanya menyalin objek hidup, manakala Mark-Sweep hanya membersihkan objek mati. Objek hidup hanya menyumbang sebahagian kecil daripada generasi baharu, dan objek mati hanya menyumbang sebahagian kecil daripada generasi lama Inilah sebabnya dua kaedah kitar semula boleh mengendalikannya dengan cekap. Gambarajah skematik selepas Mark-Sweep ditanda dalam ruang generasi lama adalah seperti berikut. Bahagian hitam ditandakan sebagai objek mati
Masalah terbesar dengan Mark-Sweep ialah selepas menanda dan mengosongkan ruang, ruang memori akan menjadi tidak berterusan. Pemecahan memori jenis ini akan menyebabkan masalah untuk peruntukan memori seterusnya Apabila objek besar perlu diperuntukkan, semua ruang yang berpecah tidak akan dapat menyelesaikan peruntukan, dan pengumpulan sampah akan dicetuskan terlebih dahulu, dan kitar semula ini tidak diperlukan.
Mark-Compact direka untuk menyelesaikan masalah pemecahan memori Mark-Sweep. Mark-Compact bermaksud kompilasi tanda, yang berkembang daripada Mark-Sweep. Perbezaannya ialah selepas objek ditandakan sebagai mati, semasa proses pembersihan, objek hidup dialihkan ke satu hujung Selepas pergerakan selesai, ingatan di luar sempadan terus dibersihkan. Gambarajah skematik selepas menanda dan menggerakkan objek hidup Grid putih ialah objek hidup, grid gelap ialah objek mati, dan grid cahaya ialah lubang yang ditinggalkan selepas objek hidup dialihkan.
Selepas melengkapkan pergerakan, anda boleh terus mengosongkan kawasan memori di belakang objek paling kanan yang masih hidup untuk melengkapkan kitar semula.
Dalam strategi kitar semula V8, Mark-Sweep dan Mark-Compact digunakan secara gabungan.
Perbandingan mudah tiga algoritma kutipan sampah utama
回收算法 | Mark-Sweep | Mark-Compact | Scavenge |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(有碎片) | 少(无碎片) | 双倍空间(无碎片) |
是否移动对象 | 否 | 是 | 是 |
由于Mark-Compact需要移动对象,所以它的执行速度不可能很快,所以在取舍上,v8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时才使用Mark-Compact
Incremental Marking
为了避免出现Js应用逻辑与垃圾回收器看到的不一致情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为称为“全停顿”(stop-the-world).在v8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大。但v8的老生代通常配置得较大,且存活对象较多,全堆垃圾回收(full垃圾回收)的标记、清理、整理等动作造成的停顿就会比较可怕,需要设法改善
为了降低全堆垃圾回收带来的停顿时间,v8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一“步进”,就让Js应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。下图为:增量标记示意图
v8在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。 v8后续还引入了延迟清理(lazy sweeping)与增量式整理(incremental compaction),让清理与整理动作也变成增量式的。同时还计划引入标记与并行清理,进一步利用多核性能降低每次停顿的时间。
在启动时添加--trace_gc
参数。在进行垃圾回收时,将会从标准输出中打印垃圾回收的日志信息。
node --trace_gc -e "var a = [];for (var i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log
在Node启动时使用--prof参数,可以得到v8执行时的性能分析数据,包含了垃圾回收执行时占用的时间。以下面的代码为例
// test.js for (var i = 0; i < 1000000; i++) { var a = {}; } node --prof test.js
会生成一个v8.log日志文件
如何让垃圾回收机制更高效地工作
在js中能形成作用域的有函数调用、with以及全局作用域
如下代码:
var foo = function(){ var local = {}; }
foo()函数在每次被调用时会创建对应的作用域,函数执行结束后,该作用域会被销毁。同时作用域中声明的局部变量分配在该作用域上,随作用域的销毁而销毁。只被局部变量引用的对象存活周期较短。在这个示例中,由于对象非常小,将会被分配在新生代中的From空间中。在作用域释放后,局部变量local失效,其引用的对象将会在下次垃圾回收时被释放
标识符,可以理解为变量名。下面的代码,执行bar()函数时,将会遇到local变量
var bar = function(){ console.log(local); }
js执行时会查找该变量定义在哪里。先查找的是当前作用域,如果在当前作用域无法找到该变量的声明,会向上级的作用域里查找,直到查到为止。
在下面的代码中
var foo = function(){ var local = 'local var'; var bar = function(){ var local = 'another var'; var baz = function(){ console.log(local) }; baz() } bar() } foo()
baz()函数中访问local变量时,由于作用域中的变量列表中没有local,所以会向上一个作用域中查找,接着会在bar()函数执行得到的变量列表中找到了一个local变量的定义,于是使用它。尽管在再上一层的作用域中也存在local的定义,但是不会继续查找了。如果查找一个不存在的变量,将会一直沿着作用域链查找到全局作用域,最后抛出未定义错误。
如果变量是全局变量(不通过var声明或定义在global变量上),由于全局作用域需要直到进程退出才能释放,此时将导致引用的对象常驻内存(常驻在老生代中)。如果需要释放常驻内存的对象,可以通过delete操作来删除引用关系。或者将变量重新赋值,让旧的对象脱离引用关系。在接下来的老生代内存清除和整理的过程中,会被回收释放。示例代码如下:
global.foo = "I am global object" console.log(global.foo);// => "I am global object" delete global.foo; // 或者重新赋值 global.foo = undefined; console.log(global.foo); // => undefined
虽然delete操作和重新赋值具有相同的效果,但是在V8中通过delete删除对象的属性有可能干扰v8的优化,所以通过赋值方式解除引用更好。
作用域链上的对象访问只能向上,外部无法向内部访问。
js实现外部作用域访问内部作用域中变量的方法叫做闭包。得益于高阶函数的特性:函数可以作为参数或者返回值。
var foo = function(){ var bar = function(){ var local = "局部变量"; return function(){ return local; } } var baz = bar() console.log(baz()) }
在bar()函数执行完成后,局部变量local将会随着作用域的销毁而被回收。但是这里返回值是一个匿名函数,且这个函数中具备了访问local的条件。虽然在后续的执行中,在外部作用域中还是无法直接访问local,但是若要访问它,只要通过这个中间函数稍作周转即可。
闭包是js的高级特性,利用它可以产生很多巧妙的效果。它的问题在于,一旦有变量引用这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,作用域中产生的内存占用也不会得到释放。
无法立即回收的内存有闭包和全局变量引用这两种情况。由于v8的内存限制,要注意此变量是否无限制地增加,会导致老生代中的对象增多。
会存在一些认为会回收但是却没有被回收的对象,会导致内存占用无限增长。一旦增长达到v8的内存限制,将会得到内存溢出错误,进而导致进程退出。
process.memoryUsage()可以查看内存使用情况。除此之外,os模块中的totalmem()和freemem()方法也可以查看内存使用情况
调用process.memoryUsage()可以看到Node进程的内存占用情况
rss是resident set size的缩写,即进程的常驻内存部分。进程的内存总共有几部分,一部分是rss,其余部分在交换区(swap)或者文件系统(filesystem)中。
除了rss外,heapTotal和heapUsed对应的是v8的堆内存信息。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 = 50 * 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 < 15; j++) { showMem(); total.push(useMem()) } showMem();
在内存达到最大限制值的时候,无法继续分配内存,然后进程内存溢出了。
os模块中的totalmem()和freemem()这两个方法用于查看操作系统的内存使用情况,分别返回系统的总内存和闲置内存,以字节为单位
通过process.memoryUsage()的结果可以看到,堆中的内存用量总是小于进程的常驻内存用量,意味着Node中的内存使用并非都是通过v8进行分配的。将那些不是通过v8分配的内存称为堆外内存
将上面的代码里的Array变为Buffer,将size变大
var useMem = function () { var size = 200 * 1024 * 1024; var buffer = Buffer.alloc(size); // new Buffer(size)是旧语法 for (var i = 0; i < size; i++) { buffer[i] = 0 } return buffer }
输出结果如下:
内存没有溢出,改造后的输出结果中,heapTotal与heapUsed的变化极小,唯一变化的是rss的值,并且该值已经远远超过v8的限制值。原因是Buffer对象不同于其它对象,它不经过v8的内存分配机制,所以也不会有堆内存的大小限制。意味着利用堆外内存可以突破内存限制的问题
Node的内存主要由通过v8进行分配的部分和Node自行分配的部分构成。受v8的垃圾回收限制的只要是v8的堆内存。
Node对内存泄漏十分敏感,内存泄漏造成的堆积,垃圾回收过程中会耗费更多的时间进行对象扫描,应用响应缓慢,直到进程内存溢出,应用崩溃。
在v8的垃圾回收机制下,大部分情况是不会出现内存泄漏的,但是内存泄漏通常产生于无意间,排查困难。内存泄漏的情况不尽相同,但本质只有一个,那就是应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象。通常原因有如下几个:
缓存在应用中的作用十分重要,可以十分有效地节省资源。因为它的访问效率要比 I/O 的效率高,一旦命中缓存,就可以节省一次 I/O时间。
对象被当作缓存来使用,意味着将会常驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,导致垃圾回收在进行扫描和整理时,对这些对象做无用功。
Js开发者喜欢用对象的键值对来缓存东西,但这与严格意义上的缓存又有着区别,严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。是一种以内存空间换CPU执行时间。示例代码如下:
var cache = {}; var get = function (key) { if (cache[key]) { return cache[key]; } else { // get from otherwise } }; var set = function (key, value) { cache[key] = value; };
所以在Node中,拿内存当缓存的行为应当被限制。当然,这种限制并不是不允许使用,而是要小心为之。
为了解决缓存中的对象永远无法释放的问题,需要加入一种策略来限制缓存的无限增长。可以实现对键值数量的限制。下面是其实现:
var LimitableMap = function (limit) { this.limit = limit || 10; this.map = {}; this.keys = []; }; var hasOwnProperty = Object.prototype.hasOwnProperty; LimitableMap.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); } map[key] = value; }; LimitableMap.prototype.get = function (key) { return this.map[key]; }; module.exports = LimitableMap;
记录键在数组中,一旦超过数量,就以先进先出的方式进行淘汰。
直接将内存作为缓存的方案要十分慎重。除了限制缓存的大小外,另外要考虑的事情是,进程之间无法共享内存。如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用是一种浪费。
如何使用大量缓存,目前比较好的解决方案是采用进程外的缓存,进程自身不存储状态。外部的缓存软件有着良好的缓存过期淘汰策略以及自有的内存管理,不影响Node进程的性能。它的好处多多,在Node中主要可以解决以下两个问题。
目前,市面上较好的缓存有Redis和Memcached。
队列在消费者-生产者模型中经常充当中间产物。这是一个容易忽略的情况,因为在大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生。但是一旦消费速度低于生产速度,将会形成堆积, 导致Js中相关的作用域不会得到释放,内存占用不会回落,从而出现内存泄漏。
解决方案应该是监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员。另一个解决方案是任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个下限值。
常见的工具
由于Node的内存限制,操作大文件也需要小心,好在Node提供了stream模块用于处理大文件。
stream模块是Node的原生模块,直接引用即可。stream继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。它分可读和可写两种。Node中的大多数模块都有stream的应用,比如fs的createReadStream()和createWriteStream()方法可以分别用于创建文件的可读流和可写流,process模块中的stdin和stdout则分别是可读流和可写流的示例。
由于V8的内存限制,我们无法通过fs.readFile()和fs.writeFile()直接进行大文件的操作,而改用fs.createReadStream()和fs.createWriteStream()方法通过流的方式实现对大文件的操作。下面的代码展示了如何读取一个文件,然后将数据写入到另一个文件的过程:
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(); }); // 简洁的方式 var reader = fs.createReadStream('in.txt'); var writer = fs.createWriteStream('out.txt'); reader.pipe(writer);
可读流提供了管道方法pipe(),封装了data事件和写入操作。通过流的方式,上述代码不会受到V8内存限制的影响,有效地提高了程序的健壮性。
如果不需要进行字符串层面的操作,则不需要借助V8来处理,可以尝试进行纯粹的Buffer操作,这不会受到V8堆内存的限制。但是这种大片使用内存的情况依然要小心,即使V8不限制堆内存的大小,物理内存依然有限制。
更多node相关知识,请访问:nodejs 教程!
Atas ialah kandungan terperinci Artikel tentang kawalan memori dalam Node. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!