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

JavaScript中的垃圾收集和内存泄漏

时间:2023-04-02 16:15:48 HTML

Preamble程序需要内存才能运行。只要程序需要,操作系统或运行时就必须提供内存。所谓内存泄漏简单来说就是不再使用的内存没有及时释放。为了更好的避免内存泄漏,我们首先介绍Javascript垃圾回收机制。在C、C++等语言中,开发者可以直接控制内存的申请和回收。但是在Java、C#、JavaScript语言中,变量内存空间的申请和释放都是由程序自己处理的,开发者不需要关心。也就是说,Javascript有一个自动垃圾收集机制(GarbageCollection)。想阅读更多优质文章,请戳GitHub博客,一年五十篇优质文章等你来!1.垃圾回收的必要性以下这段话引自《JavaScript权威指南(第四版)》  由于字符串、对象、数组没有固定大小,只有知道大小才能动态分配存储。每次JavaScript程序创建字符串、数组或对象时,解释器都必须分配内存来存储该实体。只要内存是这样动态分配的,它最终必须被释放,这样才能被重用,否则,JavaScript解释器将消耗掉系统中所有可用的内存,导致系统崩溃。这段话解释了为什么系统需要垃圾回收。与C/C++不同,JavaScript有自己的一套垃圾回收机制。JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放它们占用的内存,但这个过程并不总是这样,因为开销比较大,所以垃圾回收器会周期性地根据以固定的时间间隔执行。vara="泛舟波涛";varb="前端工匠";vara=b;//改写a的代码并运行后,字符串“boatinginthewaves”失去了引用(在引用之前被使用),系统检测到这一点后,释放该字符串的存储空间,以便它可以被重用。2.垃圾回收机制垃圾回收机制如何知道哪些内存不再需要了?垃圾回收有两种方式:标记清除和引用计数。引用计数不太常用,标记清除比较常用。1.标记清除这是javascript中最常用的垃圾回收方式。当一个变量进入执行环境时,它被标记为“进入环境”。从逻辑上讲,进入环境的变量占用的内存永远不会被释放,因为只要执行流进入相应的环境,它们就可能被使用。当变量离开环境时,它被标记为“离开环境”。垃圾收集器在运行时会标记所有存储在内存中的变量。然后它会剥离环境中的变量和环境中变量引用的标记。之后被标记的变量将被视为要删除的变量,因为环境中的变量无法再访问这些变量。终于。垃圾收集器完成内存清理,销毁那些标记的值,回收它们占用的内存空间。让我们用一个例子来解释这个方法:varm=0,n=19//将m,n,add()标记为进入环境。add(m,n)//将a,b,c标记为进入环境。console.log(n)//a,b,c被标记为离开环境,等待垃圾回收。functionadd(a,b){a++varc=a+breturnc}2.引用计数所谓“引用计数”就是语言引擎有一个“引用表”,它存储了内存中的所有资源(通常是各种值)的引用。如果一个值的引用计数为0,则表示该值不再被使用,可以释放内存。上图中,左下角的两个值没有任何引用,所以可以释放。如果一个值不再需要,但引用数不为0,垃圾回收机制就无法释放这块内存,从而导致内存泄漏。vararr=[1,2,3,4];arr=[2,4,5]console.log('乘风破浪');上面代码中,数组[1,2,3,4]是一个值,会占用内存。变量arr是对该值的唯一引用,所以引用计数为1。下面的代码中虽然没有使用arr,但它会继续占用内存。至于如何释放内存,我们下面会介绍。第三行代码,数组[1,2,3,4]引用的变量arr得到另一个值,则数组[1,2,3,4]的引用数减1,并且它的引用计数现在如果变成0,说明没有办法访问到这个值,那么它占用的内存空间就可以回收了。但是引用计数有一个最大的问题:循环引用函数func(){letobj1={};让obj2={};obj1.a=obj2;//obj1引用obj2obj2.a=obj1;//obj2referencesobj1}函数func执行时,返回值是undefined,所以整个函数和内部变量应该被回收,但是根据引用计数的方法,obj1和obj2的引用计数不为0,所以它们不会被回收。要解决循环引用,最好在不使用时手动将它们设置为空。上面的例子可以做到这一点:obj1=null;对象2=空;3、哪些情况会导致内存泄漏?虽然JavaScript会自动收集垃圾,但是如果我们的代码写得不当,变量就会一直处于“进入环境”的状态,无法被回收。下面是一些常见的内存泄漏情况:1.意外的全局变量functionfoo(arg){bar="thisisahiddenglobalvariable";}bar没有声明,会变成全局变量,当它关闭时页面关闭之前不会被释放。另一个意想不到的全局变量可能是这样创建的:在JavaScript文件头末尾加入'usestrict'可以避免此类错误。启用JavaScript的严格模式解析以避免意外的全局变量。2.忘记定时器或回调函数varsomeResource=getData();setInterval(function(){varnode=document.getElementById('Node');if(node){//处理节点和someResourcenode.innerHTML=JSON.stringify(一些资源));}},1000);这种代码很常见。如果将id为Node的元素从DOM中移除,定时器仍然存在。同时,由于回调函数中包含了对someResource的引用,定时器外的someResource也不会被释放。3.闭包函数bindEvent(){varobj=document.createElement('xxx')obj.onclick=function(){//即使是空函数}}闭包可以维护函数中的局部变量,使其不释放。上例中定义事件回调时,由于函数是在函数内部定义的,而函数内部-事件回调是指外部函数,所以形成了一个闭包。//在functionbindEvent()外部定义事件处理函数{varobj=document.createElement('xxx')obj.onclick=onclickHandler}//或者在定义事件处理函数的外部函数中删除对dom的引用functionbindEvent(){varobj=document.createElement('xxx')obj.onclick=function(){//即使是空函数}obj=null}解决办法是在外部定义事件处理函数,去掉Closure,或者在定义事件处理程序的外部函数中,删除对dom的引用。4.未清理的DOM元素引用有时保留DOM节点的内部数据结构很有用。如果要快速更新表的几行,将DOM的每一行存储为字典(JSON键值对)或数组是有意义的。此时,对同一个DOM元素有两个引用:一个在DOM树中,一个在字典中。如果您决定将来删除这些行,则需要清除这两个引用。varelements={button:document.getElementById('button'),image:document.getElementById('image'),text:document.getElementById('text')};functiondoStuff(){image.src='http://some.url/image';按钮.点击();console.log(text.innerHTML);}functionremoveButton(){document.body.removeChild(document.getElementById('button'));//此时,仍然有对#button//elements字典的全局引用。按钮元素还在内存中,无法被GC回收。}虽然我们用removeChild移除了按钮,但是#button的引用仍然保存在elements对象中,换句话说,DOM元素还在内存中。4、内存泄漏的识别方法查看新版chrome的性能:步骤:打开开发者工具Performance,查看Screenshots和内存左上角的小圆点开始录制(record)和停止录制。可以看到图中Heap对应的部分垃圾回收的周期,从内存的周期性下降也可以看出。如果垃圾回收后的最低值(我们称之为min)并且min一直在上升,那么肯定存在严重的内存泄漏问题。避免内存泄漏的一些方法:减少不需要的全局变量,或者生命周期长的对象,及时对无用数据进行垃圾回收,注意程序逻辑,避免“死循环”之类的,避免创建太长许多对象。遵循一个原则:不用的东西及时归还五、垃圾回收使用场景优化1、Array数组优化给数组对象赋值[]是清空数组的快捷方式(例如:arr=[];),但是需要注意该方法创建了一个新的空对象并将原来的数组对象变成了一小块内存垃圾!其实将数组的长度赋值为0(arr.length=0)也可以达到清空数组的目的,同时实现数组的复用,减少内存垃圾的产生。constarr=[1,2,3,4];console.log('乘风破浪');arr.length=0//数字可以直接清零,数组类型不变。//arr=[];虽然把a变量做成了一个空数组,但是在堆上重新申请了一个空数组对象。2、尽量复用对象,尤其是在循环等地方新建对象,能复用就复用。未使用的对象应尽可能设置为null,并尽快进行垃圾回收。vart={}//每次循环都会创建一个新对象。for(vari=0;i<10;i++){//vart={};//每次循环都会创建一个新对象。t.age=19t.name='123't.index=iconsole.log(t)}t=null//如果对象不再使用,立即置为null;等待垃圾收集。3.循环中的函数表达式可以复用,最好放在循环外。//最好不要在循环中使用函数表达式。for(vark=0;k<10;k++){vart=function(a){//函数对象创建了10次。console.log(a)}t(k)}//推荐使用functiont(a){console.log(a)}for(vark=0;k<10;k++){t(k)}t=null向大家推荐一款好用的BUG监控工具Fundebug,欢迎免费试用!欢迎关注公众号:前端工匠,让我们一起见证你的成长!参考资料珠峰架构课程(强烈推荐)JavaScript进阶内幕(js进阶)JavaScript垃圾回收机制JavaScript内存泄漏教程JavaScript权威指南(第四版)JavaScript中的垃圾回收4种JavaScript内存泄漏以及如何避免