当前位置: 首页 > 科技观察

中高级前端必须了解的JS中的内存管理

时间:2023-03-14 22:15:29 科技观察

中高级前端必须了解的JS中的内存管理对于JavaScript来说,内存是在创建变量(对象、字符串等)时分配的,当不再使用时“自动”释放内存。这种自动释放内存的过程称为垃圾回收。由于自动垃圾回收机制的存在,大多数Javascript开发者觉得自己可以不关心内存管理,所以在某些情况下会导致内存泄漏。内存生命周期JS环境下分配的内存有如下声明周期:内存分配:当我们声明变量、函数、对象时,系统会自动为其分配内存内存使用:即读写内存,即、使用变量、函数等内存回收:使用后,垃圾回收机制自动回收不再使用的内存JS内存分配为了避免程序员费心去分配内存,JavaScript在定义变量时就完成了内存分配。varn=123;//为数值变量分配内存vars="azerty";//为字符串分配内存varo={a:1,b:null};//为对象及其包含的值分配内存//为数组及其包含的值分配内存(就像对象一样)vara=[1,null,"abra"];functionf(a){returna+2;}//为函数分配内存(可调用对象)//函数表达式也可以分配一个对象someElement.addEventListener('click',function(){someElement.style.backgroundColor='blue';},false);一些函数调用导致对象内存的分配:vard=newDate();//分配一个Date对象vare=document.createElement('div');//分配一个DOM元素一些方法分配新变量或新对象:vars="azerty";vars2=s.substr(0,3);//s2是一个新字符串//因为字符串是不变的,//JavaScript可能决定不分配内存,//只存储范围[0-3]。vara=["ouaisouais","nannan"];vara2=["generation","nannan"];vara3=a.concat(a2);//新数组有四个元素,是a连接到的结果a2在JS内存使用中使用values的过程其实就是一个读写分配内存的操作。读写可能是写一个变量或者一个对象的属性值,甚至是传递函数参数。vara=10;//分配内存console.log(a);//使用JS内存回收内存JS有自动垃圾回收机制,那么这个自动垃圾回收机制的原理是什么?其实很简单,把那些不再使用的值找出来,然后释放它占用的内存即可。大多数内存管理问题都在这个阶段。这里最困难的任务是找到不再使用的变量。不再需要使用的变量是生命周期结束的变量。它们是局部变量。局部变量仅在函数执行期间存在。当函数结束运行并且没有其他引用(闭包)时,该变量将被标记为回收。全局变量的生命周期直到浏览器卸载页面才会结束,也就是说全局变量不会被垃圾回收。由于自动垃圾回收机制的存在,开发者可以不关心也不关注内存释放相关的问题,但是无用内存的释放是客观存在的。不幸的是,即使不考虑垃圾回收对性能的影响,目前最先进的垃圾回收算法也无法智能回收所有极端情况。接下来,我们来探讨一下JS垃圾回收的机制。垃圾收集引用垃圾收集算法在很大程度上依赖于引用的概念。在内存管理的上下文中,如果一个对象可以访问另一个对象(隐式或显式),则它被称为引用另一个对象的对象。例如,一个Javascript对象有对其原型的引用(隐式引用)和对其属性的引用(显式引用)。这里,“对象”的概念不仅特指JavaScript对象,还包括函数作用域(或全局词法作用域)。引用计数垃圾收集这是最原始的垃圾收集算法。引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有引用指向它。如果没有其他对象指向它,则不再需要该对象。varo={a:{b:2}};//创建了两个对象,一个作为另一个的属性被引用,另一个赋值给变量o//很明显,它们都不能被垃圾回收varo2=o;//o2变量是对“thisobject”的第二个引用o=1;//原来对“Thisobject”的引用o现在被o2varoa=o2.a;//对“Thisobject”的引用"aattribute//现在,“这个对象”有两个引用,一个是o2,另一个是oao2="yo";//原来的对象现在是零引用//他可以被垃圾回收了//但是对象的它的属性a仍然被oa引用,所以oa=null不能被回收;//属性a的对象现在也被零引用了//可以被垃圾回收从上面可以看出,引用计数算法是一种简单高效的算法。但是它有一个致命的问题:循环引用。如果两个对象相互引用,即使它们不再被使用,垃圾回收也不会回收它们,从而导致内存泄漏。我们来看一个循环引用的例子:functionf(){varo={};varo2={};o.a=o2;//o引用o2o2.a=o;//o2引用o这里return"azerty";}F();上面我们声明了一个函数f,它包含两个相互引用的对象。在调用函数结束后,对象o1和o2实际上离开了函数的范围,因此不再需要。但是根据引用计数的原理,它们之间的相互引用仍然存在,所以这部分内存不会被回收,内存泄漏是不可避免的。让我们看一个实际的例子:vardiv=document.createElement("div");div.onclick=function(){console.log("点击");};上面的JS写法很常见,创建一个DOM元素,绑定一个点击事件。这时候变量div就有了对事件处理器的引用,而事件处理器也有对div的引用!(可以在函数内访问div变量)。出现了顺序引用,按照上面提到的算法,这部分内存必然会泄露。为了解决循环引用带来的问题,现代浏览器通过标记清除算法实现了垃圾回收。标记清除算法标记清除算法将“不再使用的对象”定义为“无法访问的对象”。简单来说就是从根(在JS中是全局对象)开始,定时扫描内存中的对象。所有可以从根到达的对象仍然需要被使用。无法从根访问的对象被标记为不再使用并稍后收集。从这个概念可以看出,不可达对象包含了无引用对象的概念(没有任何引用的对象也是不可达对象)。但反过来不一定成立。工作流程:垃圾收集器会在运行时标记所有存储在内存中的变量。从根开始,清除可触摸物体的标记。那些仍然有标志的变量被认为是被删除的。***垃圾收集器会执行最后一步内存清理,销毁那些标记的值,回收它们占用的内存空间。循环引用不再是问题看看前面循环引用的例子:functionf(){varo={};varo2={};o.a=o2;//o引用o2o2.a=o;//o2引用oreturn”阿泽蒂";}f();函数调用返回后,这两个循环引用的对象在垃圾回收时无法再从全局对象中获取它们的引用。因此,它们将被垃圾收集器收集。内存泄漏什么是内存泄漏?程序需要内存才能运行。操作系统或运行时必须在程序请求时提供内存。对于一个持续运行的服务进程(守护进程)来说,不再使用的内存必须及时释放。否则,内存占用会越来越高,轻则影响系统性能,重则导致进程崩溃。从本质上讲,内存泄漏就是程序由于疏忽或错误而未能释放不再使用的内存,从而造成内存的浪费。识别内存泄漏的经验法则是,如果连续五次垃圾回收后,每次内存使用量都变大,则存在内存泄漏。这就需要实时查看内存使用情况。在Chrome浏览器中,我们可以通过这种方式查看内存使用情况。打开开发者工具,选择Performance面板,勾选最上方的Memory,点击左上角的record按钮,即可对页面进行各种操作。模拟用户一段时间的使用后,点击对话框中的停止按钮,面板上会显示这段时间的内存使用情况,可以看到一个效果图:我们有两种方式来判断是否有内存leak:多次快照后,比较每个快照中的内存占用情况,如果呈上升趋势,则可以认为存在内存泄漏。某次快照后,查看当前内存使用趋势图。如果趋势不稳定,呈上升趋势,则可以认为存在内存泄漏。使用服务器环境中Node提供的process.memoryUsage方法查看内存状态console.log(process.memoryUsage());//{//rss:27709440,//heapTotal:5685248,//heapUsed:3449392,//external:8772//}process.memoryUsage返回一个包含Node进程内存使用信息的对象。该对象包含四个字段,单位为byte,含义如下:rss(residentsetsize):所有内存使用,包括指令区和栈。heapTotal:“堆”占用的内存,包括已用和未用。heapUsed:已使用的堆部分。external:V8引擎内部C++对象占用的内存。判断内存泄漏,以heapUsed字段为准。常见内存泄漏案例Unexpectedglobalvariablefunctionfoo(){bar1='sometext';//没有声明的变量其实是全局变量=>window.bar1this.bar2='sometext'//全局变量=>window.bar2}foo();在此示例中,意外创建了两个全局变量bar1和bar2。被遗忘的定时器和回调函数在很多库中,如果使用观察者模式,都会提供一个回调方法来调用一些回调函数。记得回收这些回调函数。举一个setInterval的例子:varserverData=loadData();setInterval(function(){varrenderer=document.getElementById('renderer');if(renderer){renderer.innerHTML=JSON.stringify(serverData);}},5000);//每5秒调用一次如果后面的renderer元素被移除,整个timer其实是没有作用的。但是如果不回收定时器,整个定时器还是有效的,不仅定时器无法被内存回收,定时器函数中的依赖也无法回收。serverData在这种情况下也不能被回收。闭包在JS开发中,我们经常使用闭包,一种内部函数,可以访问包含它的外部函数中的变量。在以下情况下闭包也会导致内存泄漏:};theThing={longStr:newArray(1000000).join('*'),someMethod:function(){console.log("message");}};};setInterval(replaceThing,1000);在这段代码中,每次调用replaceThing时,theThing都会获得一个包含巨大数组的对象和一个用于新闭包someMethod的对象。同样未使用的是引用originalThing的闭包。这个范例的关键是闭包共享作用域。虽然unused可能没有被调用过,但是someMethod可能被调用了,会导致其内存回收失败。随着这段代码被反复执行,内存会不断增长。DOM引用很多时候,我们在对Dom进行操作的时候,都会将Dom的引用保存在一个数组或者Map中。varelements={image:document.getElementById('image')};functiondoStuff(){elements.image.src='http://example.com/image_name.png';}functionremoveImage(){document.body.removeChild(document.getElementById('image'));//此时我们还有对#image的引用,Image元素,内存仍然无法回收。}在上面的例子中,即使我们移除了image元素,仍然存在对图像元素的引用仍然无法对齐以进行内存回收。另一点需要注意的是对Dom树的叶节点的引用。例如:如果我们在一个表中引用了一个td元素,一旦在Dom中删除了整个表,我们直观的感觉内存回收应该回收除了被引用的td之外的其他元素。但实际上,这个td元素是整个表格的一个子元素,并保持着对其父元素的引用。这将导致无法为整个表回收内存。所以我们必须小心引用Dom元素。如何避免内存泄露记住一个原则:没有使用的东西要及时归还。减少不必要的全局变量,使用严格模式避免意外创建全局变量。使用完数据后,及时取消引用(闭包中的变量、dom引用、计时器清除)。组织好你的逻辑,避免无限循环等导致浏览器卡顿和崩溃的问题。