当前位置: 首页 > Web前端 > HTML5

热门面试题:Node.js中的垃圾回收?

时间:2023-04-05 17:26:37 HTML5

序言极尽倾注,深陷其中,界限分明。分享的文章纯属个人观点。如果有不准确的地方或者需要讨论的地方,欢迎大家评论交流,和同学一起学习~欢迎关注“前端进阶”步圈’公众号,一起探索学习前端技术。..回复公众号加群或者扫码,可以加入前端交流学习群,长期交流学习...回复公众号加好友,可以加为一个朋友。文章分为最终总结和一步一步的解释,可以根据需要查看。废话不多说,先总结一下。总结:Node.js运行的内存属于ChromeV8管理的内存。Javascirpt中创建的对象存储在堆内存中。V8的垃圾回收机制根据对象的存活时间将内存分为新生代和老年代。NewGeneration:简单来说就是存放生存时间较短的对象的地方。主要用处是semispace,将内存分成两个空间:From空间和To空间。比如我们声明了一个新的对象,这个新的对象就会被放到From空间中。当From空间快满时,会遍历所有对象,将From空间的活动对象复制到To空间。在这个过程中,如果一个对象被复制了很多次,那么它就会被认为是一个存活时间较长的对象,会被放入老年代。老年代:相反,是存放寿命较长的对象的地方。主要使用Mark-Sweep算法。它会从执行栈和全局对象中找到所有可访问的对象,并将它们标记为活动对象。清除(这个过程是将被清除对象的内存标记为空闲),最后,释放空闲内存。一般来说,Node的内存机制分为两部分:ChromeV8管理的部分(Javascript使用的部分),系统底层管理的部分(C++/C使用的部分),以及Node程序运行占用的ChromeV8内存管理机制。里面的所有内存称为常驻内存,常驻内存由以下部分组成:代码区:存放要执行的代码片段栈:存放局部变量堆:存放对象和闭包上下文,V8使用的垃圾回收机制管理堆memoryOff-heapmemory:不由V8分配,也不由V8管理。就是Buffer对象的数据存放的地方A:除了堆内存,其余内存由V8管理。堆栈的分配和回收非常简单。当程序离开某个作用域时,它的栈指针向下移动(即回滚),整个作用域的局部变量就会被弹出栈,内存就会被回收。最复杂的部分是堆管理。V8使用垃圾回收机制来管理堆内存。也是开发中可能导致内存泄漏的部分,是开发者关注的重点。MemoryC/C++部分是Node原生的部分,和前端的js有本质区别,包括核心运行时库。在一些核心模块的加载过程中,Node会调用一个叫做js2c的工具。本工具会将核心js模块代码以C数组的形式存放在内存中,以提高运行效率。在这部分,我们同样没有内存使用限制,但是使用大量内存作为C/C++扩展的风险也是显而易见的。C/C++没有内存回收机制。作为一个没有C/C++功底的纯前端程序员,不建议使用这部分,因为C/C++模块非常强大,如果对对象生命周期的理解不到位,在使用大内存对象,很容易造成内存溢出,导致整个Node甚至系统崩溃。使用大内存的安全方法是使用Buffer对象。Node中的js引擎也是Chrome的V8引擎,所以垃圾回收机制也属于V8内部的垃圾回收机制。js中的对象存放在堆内存中。当一个进程被创建时,会分配一个默认的堆内存。当对象越来越大时,堆内存会动态扩展。如果达到最大限制,堆内存将溢出抛出错误,然后终止node.js进程。V8的垃圾回收机制根据对象的存活时间采用不同的算法。内存主要分为新生代和老年代。新生代(存活时间短的对象):新生代内存分为两个空间(每一部分空间称为半空间)从空间:新的生命对象将存储在这里到空间:作为移动空间新声明的对象会被放入From空间,From空间中的对象排列紧密。通过指针,前一个对象靠近下一个对象,所以内存是连续的,我们不用担心内存碎片。问:什么是内存碎片?内存碎片分为内部碎片和外部碎片两种内部碎片:系统为某个函数分配了一定的内存,但是该函数的最终执行并没有用完系统分配的内存,剩下的内存中的碎片称为内存碎片internalfragments。ExternalFragmentation:存在一些连续内存的浪费,这些内存太小而无法被系统分配给一个函数。当From空间快满时,活动对象会从From空间遍历复制到To空间。这时候From空间会空,From和To会互换。如果一个对象被复制了很多次,那么它就会被认为是长寿对象,会被移入老年代。A:这种基于拷贝的算法的好处是可以很好的处理内存碎片问题。缺点是作为移动空间的位置会浪费一些空间。另外,因为复制比较耗时,不适合分配过多的内存空间。更多的是做一种辅助垃圾回收。将一个区域的存活对象(Scavenge算法:一种基于复制的算法)复制到另一个区域,释放原区域的内存,如此重复。当一个对象在多次复制中幸存下来时,它就会被移入老年代。Scavenge算法的原理(具体实现采用Cheney算法):在垃圾回收过程中,将幸存的对象在两个半空间之间进行复制。优点:时间短。缺点:只能使用一半的堆内存。新生代对象生命周期短,适合该算法。老年代(存活时间长的对象):老年代的空间远大于新生代,存放一些存活时间长的对象,采用Mark-Sweep(标记清除)算法。标记清除过程:从根集合RootSet(执行栈和全局对象)中找出所有可访问的对象,标记为活动对象。标记完毕后,就是清场阶段,将未标记的对象清零。其实就是把这个内存地址标记为空闲。这种做法会导致空闲内存空间的碎片化。当我们创建一个大的连续对象时,就会无处安放。这时候就需要使用Mark-Compact来整合fragments的activeobjects。Mark-Compact会将所有活动对象副本移到一侧,然后边界的另一侧是一整块连续的空闲内存。考虑到Mark-Sweep和Mark-Compact耗时较长,会阻塞JavaScript线程,通常我们不会一下子全部搞定,而是使用IncrementalMarking。也就是做间歇性标记,小步走的策略,垃圾回收和应用逻辑交替进行。此外,V8还进行了并行标记和并行清洗,以提高执行效率。S:老年代采用mark-and-clear算法,遍历所有对象,对存活的对象进行标记,然后在清除阶段清除未标记的对象,最后释放清除的空间。OldgenerationNewgeneration(默认)Younggeneration(最大)64位系统1400MB32MB64MB32位系统700MB16MB32MB注:垃圾收集是影响性能的因素之一。应尽量减少垃圾回收,尤其是全堆垃圾回收对象存活时间内存空间存活时间长或常驻内存的老年代对象——max-old-space-size命令设置老年代对象内存空间的最大大小withashortsurvivaltimeofthenewgeneration–max-new-space-size命令设置了新生代内存空间的大小Size问:为什么V8引擎会把内存分为新生代和老年代?R:垃圾回收机制(GC)有很多,但没有一种适合所有场景。在实际应用中,需要根据对象生命周期的长短采用不同的算法来达到最佳效果。在V8中,内存垃圾回收机制根据对象的存活时间分为不同的代,然后针对不同的内存采用不同的高效算法。所以有新老两代。Q:为什么V8会限制堆内存的大小?R:因为V8垃圾回收机制的限制。垃圾回收会导致js线程暂停执行;内存太大,垃圾回收时间太长。在这种考虑下,直接限制了堆内存的大小。Q:如何让内存无限大?R:在Node中,使用Buffer读取超过V8内存限制的大文件。原因是Buffer对象不同于其他对象,它不经过V8的内存分配机制。这是因为Node不同于浏览器的应用场景。在浏览器中,JavaScript可以直接处理字符串来满足大部分业务需求,而Node需要处理网络流和文件I/O流,操作字符串远远不能满足传输的性能需求。R:当不需要进行字符串操作时,可以不使用v8而使用Buffer操作,这样就不会受到v8内存的限制Q:如何查看内存信息?可以通过process.memoryUsage()方法process.memoryUsage();//output:{rss:35454976,heapTotal:7127040,heapUsed:5287088,external:958852,arrayBuffers:11314}/***unit(单位):字节(byte)rss:常驻集大小(residentsetsize),包括代码片段、堆内存、栈等部分。heapTotal:V8的堆内存总大小;heapUsed:占用的堆内存;external:V8以外的内存大小,指的是C++对象占用的内存,比如Buffer数据。arrayBuffers:与ArrayBuffer和SharedArrayBuffer相关的内存大小,属于external*/Q:如何测试最大内存限制?写个脚本,用定时器,让数组一直增长,打印堆内存使用情况,直到内存溢出,抛出错误constformat=function(bytes){return(bytes/1024/1024).toFixed(2)+"MB";};constprintMemoryUsage=function(){constmemoryUsage=process.memoryUsage();console.log(`heapTotal:${format(memoryUsage.heapTotal)},heapUsed:${forma(memoryUsage.heapUsed)}`);};constbigArray=[];setInterval(function(){bigArray.push(newArray(20*1024*1024));printMemoryUsage();},500);A:不使用Buffer做测试。因为Buffer是处理二进制的Node.js特定对象,所以它没有在V8中实现。它是用C++在Node.js中实现的。不通过V8分配内存,属于堆外内存。测试说明:使用的电脑是macbookM1Pro,Node.js版本是v16.17.0,使用的V8版本是9.4.146.26-node.22(通过process.versions.v8获取)//result:heapTotal:164.81MB,heapUsed:163.93MBheapTotal:325.83MB,heapUsed:323.79MBheapTotal:488.59MB,heapUsed:483.84MB...heapTotal:4036.44MB,heapUsed:4003.37MBheapTotal:4196.45MB,heapUsed:41<63-.29最后几个MB-->[28033:0x140008000]17968毫秒:标记扫描4003.2(4036.4)->4003.1(4036.4)MB,2233.8/0.0毫秒(平均mu=0.565,当前mu=0.310)分配失败清除可能不会成功[28033:0800400]19815毫秒:标记扫描4163.3(4196.5)->4163.1(4196.5)MB,1780.3/0.0毫秒(平均mu=0.413,当前mu=0.036)分配失败清除可能不会成功<---JS堆栈跟踪--->致命ERROR:ReachedheaplimitAllocationfailed-JavaScriptheapoutofmemory...可以看到4000MB后超过了内存限制,出现堆溢出out,然后退出进程注意在我的机器上,默认最大内存是4G。实际最大内存与它运行的机器有关。如果你的机器有2G内存,最大内存会设置为1.5G。Q:Javascript部分被ChromeV8接管了吗?那么为什么仍然可以使用大量内存创建缓存呢?R:是的,Chrome和Node都使用ChromeV8作为JS引擎,但实际上他们处理的是不同的对象,Node处理的是数据提供,逻辑和I/O,而Chrome处理的是界面Rendering,数据的呈现.所以在Chrome上,几乎不可能遇到大内存的情况,而为Chrome而生的V8引擎自然不会考虑这种情况,所以才会有内存限制。现在,面对这样的情况,Node是无法接受的,所以Buffer对象是一个特殊的对象,由下层模块创建,存储在V8引擎以外的内存空间中。R:在内存方面,Buffer和V8不相上下。Q:如何高效使用内存?手动销毁变量js中有函数调用,with和全局作用域可以形成作用域。比如调用一个函数,执行后会创建和销毁对应的scope,在这个scope声明的局部变量也会被Destroyedidentifiersearch(即变量名)先搜索当前scope,然后再去到上层作用域,直到全局作用域变量被主动释放。直到进程退出,全局变量才被释放,导致被引用的对象驻留在老年代。可以使用delete删除或者assignundefined,null(delete删除对象的属性可能会干扰v8,所以赋值更好)谨慎使用闭包闭包是外部作用域访问内部作用域的一种方式,得益于高阶函数特征varfoo=function(){varbar=function(){varlocal="internalvariable";returnfunction(){returnlocal;};};varbaz=bar();console.log(baz());};//从上面的代码知道bar()返回的是一个匿名函数。一旦一个变量引用了它,它的作用域就不会被释放,直到没有引用为止。//注意:当闭包赋值给一个不可控的对象时,会造成内存泄漏。使用后给变量赋其他值或清空大内存即可使用stream。当我们需要操作大文件时,应该使用Node提供的stream及其pipeline方法,防止一次读取过多数据,占用堆空间,增加文件大小。堆内存压力。使用Buffer,Buffer是一个用于操作二进制数据的对象。不管是字符串还是图片,底层都是二进制数据,所以Buffer可以适用于任何类型的文件操作。Buffer对象本身是一个普通的对象,存储在堆中,由V8管理,但是其中存储的数据存储在堆外内存中,由C++应用程序分配,所以不受V8管理,并且不需要被V8垃圾回收。一定程度上节省了V8资源,不用关心堆内存限制。问:内存泄漏?原因:缓存,队列消费不及时,作用域没有释放等编译后缓存,因为通过exports导出(闭包),作用域不会释放,常驻老年代。当心内存泄漏。变量arr=[];exports.hello=function(){arr.push("hello"+Math.random());};//局部变量arr不断增加内存占用,不会被释放,必要时设计,提供释放接口队列状态监控生产者和消费者之间的队列长度,如果超过长度则拒绝任何异步调用应该包含超时机制内存泄漏排查工具node-heapdump安装npminstallheapdump引入varheapdump=require('heapdump');发送命令kill-USR2,heapdump会抓取一个heap内存快照,文件为heapdump-..heapsnapshot格式,是一个json文件node-memwatchvarmemwatch=require('memwatch');memwatch.on('leak',function(info){console.log('leak:');console.log(info);});memwatch.on('stats',function(stats){console.log('stats:')console.log(统计);});进程使用node-memwatch后,每次fullheap垃圾回收都会触发stats事件,会通过内存统计stats:{num_full_gc:4,//多少次fullHeap垃圾回收num_inc_gc:23,//的增量垃圾回收次数heap_compactions:4,//组织老年代的次数usage_trend:0,//使用趋势estimated_base:7152944,//估计基数current_base:7152944,//当前基数min:6720776,//Minimummax:7152944//Maximum}如果内存在连续5次垃圾回收后仍未释放,则说明存在内存泄漏,node-memwatch会触发泄漏事件。文章特殊字符描述:问号Q:(问题)答案标记R:(结果)备注标准:A:(注意事项)详细描述标记:D:(详细信息)摘要标记:S:(摘要)最后:欢迎关注「前端进阶圈」公众号,一起探索学习前端技术……回复公众号加群或扫码,即可加入前端交流学习群,长-术语交流学习...公众号回复加好友,即可加为好友