几周前,我们开始了一个深入探讨JavaScript及其工作原理的系列文章。我们相信,了解JavaScript的构成以及它们如何协同工作会带来更好的代码和应用程??序。本系列的第一篇文章重点介绍了引擎、运行时和调用堆栈。第二篇文章揭示了谷歌V8JavaScript引擎的内部机制,并提供了一些如何编写更好的JavaScript代码的建议。作为第三篇文章,本文将讨论开发人员容易忽视的另一个重要话题:内存管理。我们还将提供一些有关如何处理JavaScript内存泄漏的技巧。在SessionStack中,我们需要确保不会造成内存泄漏或增加集成Web应用程序的内存消耗。概述一些语言如C具有低级本机内存管理原语,如malloc()和free()。开发人员使用这些原语来显式分配和释放操作系统内存。相比之下,JavaScript在创建变量(对象、字符串)时会自动分配内存,并在不使用这些变量时自动释放内存。这个过程称为垃圾收集。这种“自动”释放资源的方式造成了很多混乱,让JavaScript(和其他高级语言)开发者误以为他们可以不关心内存管理。这是一个大错误。即使使用高级语言,开发人员也应该对内存管理有一定的了解(至少是基本的了解)。有时自动内存管理会出现一些问题(例如,垃圾回收实现可能存在缺陷或不足),开发人员必须了解这些问题才能找到合适的解决方案。无论您使用哪种编程语言,内存生命周期几乎总是相同的:这里是循环的每一步发生的事情的概述:这里是循环的每一步发生的事情的概述:Allocatingmemory—Memoryis由操作系统分配,允许程序使用它。在低级语言(如C)中,这是开发者需要处理的显式操作。然而,在高级语言中,这些操作是代表开发人员处理的。使用内存。要实际使用先前分配的内存,请通过在代码中操作变量来在内部读取和写入。免费内存。不使用时,可以释放内存以供重新分配。与分配内存一样,释放内存需要在低级语言中进行显式操作。如果想快速了解栈和内存的概念,可以阅读本系列的第一篇文章。什么是内存在直接讨论Javascript中的内存之前,让我们简单地讨论一下内存是什么以及它是如何工作的。在硬件中,计算机的内存包含大量触发器电路,每个触发器电路包含能够存储1位数据的晶体管。触发器由唯一标识符寻址,允许读取和覆盖它们。因此,从概念上讲,计算机内存可以认为是一个巨大的可读写数组。人类不擅长用位运算来表示我们所有的思想和算术,我们把这些小东西组织成大块头,可以用来表示数字:8位是一个字节。上面的字节是字(16位、32位)。许多东西都存储在内存中:程序中使用的所有变量和数据;程序的代码,包括操作系统的代码。编译器和操作系统协同工作以帮助开发人员完成大部分内存管理,但我们建议您了解幕后发生的事情。在编译代码时,编译器会解析原始数据类型,并预先计算出它们需要多少内存空间。然后在堆栈空间中分配所需的数量。之所以叫栈空间,是因为调用函数时,它们的内存是在已有内存之上添加的(即在栈顶添加一个栈帧,指向内部变量所在的空间)的功能被存储)。终止后,调用将按LIFO(后进先出)顺序删除。例如:intn;//4bytesintx[4];//4个元素的数组,每个元素为4bytesdoublem;//8bytes编译器立即知道需要内存4+4×4+8=28byte.这是整数和双精度数的当前大小。大约20年前,整数通常只需要2个字节,双精度数需要4个字节,并且您的代码不受底层数据类型大小的限制。编译器插入与操作系统交互的代码,以请求堆栈上必要的字节大小来存储变量。在上面的例子中,编辑器知道每个变量的确切地址。事实上,每当我们写入变量n时,它都会在内部被翻译成类似“内存地址4127963”的内容。注意,如果我们尝试访问x[4]的内存(x[4]的初始声明是一个长度为4的数组,x[4]表示第5个元素),我们将访问m的数据。那是因为我们访问的是一个数组中不存在的元素,m比数组中实际分配内存的最后一个元素x[3]还要远4个字节,最终可能的结果是读取(或覆盖)m的一些位。这肯定会对其他程序产生不良后果。当函数调用其他函数时,每个函数在调用时都会获得自己的堆栈块。函数中的所有变量都会保存在它自己的栈块中,一个程序计数器会记录变量在执行时的位置。当函数完成执行时,它的内存被释放以供其他用途。动态分配不幸的是,事情并没有那么简单,因为在编译时我们不知道一个变量需要多少内存。假设我们做这样的事情:intn=readInput();//读取用户输入...//创建一个有n个元素的数组编译器不知道这个数组需要多少内存,因为数组大小取决于用户-提供的价值。因此此时不能在栈上分配空间。程序必须在运行时向操作系统请求足够的空间。此时内存是从堆空间中分配的。下表总结了静态分配内存和动态分配内存的区别:静态分配内存和动态分配内存的区别。为了充分理解动态内存是如何分配的,我们需要花更多的时间在指针上,这可能在很大程度上偏离了本文的主题。如果您有兴趣了解更多信息,请在评论中告诉我,我可以在以后的文章中写下有关指针的更多详细信息。JavaScript中的内存分配现在让我们解释一下JavaScript中的第一步(分配内存)是如何工作的。当开发人员声明值时,JavaScript会自动分配内存。varn=374;//为值分配内存vars='sessionstack';//为字符串分配内存varo={a:1,b:null};//为对象及其包含的值分配内存vara=[1,null,'str'];//为数组及其包含的值分配内存functionf(a){returna+3;}//为函数(可调用对象)分配内存//函数expression也会分配一个ObjectsomeElement.addEventListener('click',function(){someElement.style.backgroundColor='blue';},false);//有些函数调用也会引起对象分配`vard=newDate();//allocatesaDateobject`//为一个Date对象分配内存`vare=document.createElement('div');//为一个DOM元素分配内存//方法可以分配新的值或对象vars1='sessionstack';vars2=s1.substr(0,3);//s2是一个新字符串//因为字符串是不可变的//JavaScript可能决定不分配内存//并且只存储范围0-3vara1=['str1','str2'];vara2=['str3','str4'];vara3=a1.concat(a2);//新数组有4个元素,由a1和a2相连。在JavaScript中使用内存在JavaScript中使用分配的内存本质上是在内部读写。例如,读取或写入对象的变量或属性的值,或将参数传递给函数。不再需要时释放内存大多数内存管理问题都出现在这个阶段。这里最困难的任务是找出何时不再需要分配的内存。这往往需要开发人员决定程序中的一块内存不再需要并释放它。高级语言嵌入了一种称为垃圾收集的软件。它的工作是跟踪内存的分配和使用情况,以便在某些情况下发现不再需要某些内存,它会自动释放内存。不幸的是,这个过程是一个近似过程,因为通常知道是否需要内存的问题是不可判定的(不能通过算法解决)。大多数垃圾收集器收集不再访问的内存,即指向它的所有变量都超出范围。但是,这是可以收集的内存空间集合的近似值。因为在任何时候,一个内存地址可能还有作用域内的变量指向它,但不会再被访问。垃圾收集由于无法确定是否“不再需要”某些内存,因此垃圾收集的实现存在局限性。本节解释理解主要垃圾收集算法及其局限性所必需的概念。内存引用垃圾回收算法所依赖的主要概念是引用。在内存管理的上下文中,只要一个对象显式或隐式访问另一个对象,就可以说它引用了另一个对象。例如,一个JavaScript对象引用它的原型(隐式引用),或者原型对象的一个??属性值(显式引用)。在这种情况下,“对象”的概念被扩展到比普通JavaScript对象更广泛的范围,也包括函数范围。(或全局词法作用域)词法作用域定义如何在嵌套函数中解析变量名:内部函数包括父函数的作用域,即使父函数已经返回。引用计数垃圾收集这是最简单的垃圾收集算法。当没有其他引用指向一个对象时,该对象被认为是“可收集的”。看看下面的代码:varo1={o2:{x:1}};//创建了2个对象/'o2'被'o1'作为属性引用//没有一个可以被回收varo3=o1;//'o3'是第二个变量o1=1,引用'o1'指向的对象;//现在,'o1'只有一个引用,即'o3'varo4=o3.o2;//引用to'o2'ofthe'o3'object'Property//'o2'对象此时有2个引用:一个是对象的一个??属性//另一个是'o4'o3='374';//'o1'原始对象现在有0个引用Reference//'o1'已准备好进行垃圾回收。//但是它的'o2'属性仍然被'o4'变量引用,所以'o2'不能被释放。o4=null;//'o1'中的原始'o2'属性不被其他人引用//'o2'可以被垃圾回收循环引用带来麻烦循环引用是有限制的。在下面的示例中,创建了两个对象并相互引用,从而创建了循环引用。它们在函数调用后超出范围,应该是可释放的。然而,引用计数算法考虑到这两个对象中的每一个都至少被引用一次,因此不能被回收。functionf(){varo1={};varo2={};o1.p=o2;//o1指代o2o2.p=o1;//o2指代o1\。形成循环引用}f();markclearingalgorithminordertodecideWhetheranobjectisneeded,该算法用于确定是否可以找到一个对象。该算法包含以下步骤。垃圾收集器生成一个根列表。根通常是一个全局变量,在代码中保存一个引用。在JavaScript中,window对象是一个全局变量,可以用作根。所有的根都被检查并标记为活动的(不是垃圾),所有的子变量也被递归地检查。从根元素可以到达的所有东西都不被认为是垃圾。所有未标记为活动的内存都被视为垃圾。垃圾收集器可以释放内存并将内存返回给操作系统。上图是标记去除的示意图。这个算法比上一个(引用计算)要好,因为“一个对象没有被引用”使得这个对象不可访问。相反,正如我们在循环引用的例子中看到的,对象不能被访问,不一定没有引用。截至2012年,所有浏览器都内置了标记清除垃圾收集器。在过去几年中,JavaScript垃圾收集(分代/增量/并行/并行GC)的所有改进都通过这种算法(标记-and-sweep)的改进,但不是GC算法本身的改进,也没有改变其判断对象是否可达这个目标。推荐一篇文章,里面详细介绍了跟踪垃圾回收,包括markandsweepmethod及其优化算法。循环引用不再是问题在上面的例子中(有循环引用的那个),函数执行后,这两个对象没有被任何可达的全局对象引用。因此,垃圾收集器将发现它们无法访问。这两个对象之间虽然有相互引用,但是从全局对象是无法到达的。垃圾收集器的不当行为尽管垃圾收集器很方便,但他们有自己的计划。其中之一是不确定性。换句话说,GC是不可预测的。您不可能知道何时执行收集器。这意味着程序在某些情况下可以使用比实际需要更多的内存。在其他情况下,在特别敏感的应用程序中,可能会出现短暂的暂停。大多数GC实现在内存分配期间启动收集例程,尽管不确定意味着无法确定何时执行收集工作。如果没有内存分配,大多数垃圾回收会保持空闲状态。参考下面的情况。执行一组相当大的分配。大多数(或全部)这些元素都被标记为不可访问(假设我们清除了对不再需要的缓存的引用。)不再执行分配。在这种情况下,大多数垃圾回收实现不会进行进一步的回收。也就是说,虽然有不可达的引用变量可供回收,但回收器是不会在意的。严格来说,这不是泄漏,而是比平时使用更多的内存。什么是内存泄漏?内存泄漏基本上是应用程序不再需要的内存,并且由于某种原因尚未返回操作系统或进入可用内存池。编程语言喜欢不同的内存管理方式。但是,某块内存是否被使用是一个无法判定的问题。也就是说,一块内存是否可以归还给操作系统,只有开发者才能搞清楚。一些编程语言为开发者提供了释放内存的能力。其他人则希望开发人员确切地知道一块内存何时无用。维基百科有一篇关于内存管理的非常好的文章。4常见的JavaScript内存泄漏1:全局变量JavaScript以一种有趣的方式管理未声明的变量:对未声明变量的引用在全局对象中创建了一个新变量。在浏览器的情况下,这个全局对象是窗口。换句话说:functionfoo(arg){bar="sometext";}等同于functionfoo(arg){window.bar="sometext";}如果假定bar仅引用函数foo范围内的变量,但是你忘记了,不是用var来声明,而是声明了一个意想不到的全局变量。在此示例中,泄漏一个简单的字符串不会造成太大危害,但它确实有可能变得更糟。另一种不小心创建全局变量的方法是通过this:functionfoo(){this.var1="potentialaccidentalglobal";}//Foo作为函数被调用,this指向全局变量(window)//而不是undefinedfoo();为了防止这些问题的发生,你可以使用'usestrict';在你的JavaScript文件的开头。这可以使用严格模式解析JavaScript,以防止意外的全局变量。除了不小心创建的全局变量,还有很多显式创建的全局变量。这些当然是不可回收的(除非指定为空或重新分配)。尤其是那些用来临时存放数据的全局变量非常重要。如果必须使用全局变量来存储大量数据,请确保在使用完后将其分配为null或重新分配给其他值。2:忘记定时器或回调在JavaScript中使用setInterval是很常见的。大多数库,尤其是那些提供观察者或其他接收回调的实用函数的库,都会在它们自己的实例变得无法访问之前使这些回调无法访问。但是说到setInterval,下面的代码就很常见了:serverData);}},5000);//每5秒执行一次定时器可能会导致引用不必要的节点或数据。将来可能会删除渲染器对象,从而使间隔处理程序中的整个块变得无用。但是由于间隔仍然有效,处理程序不能被回收(除非间隔被停止)。如果interval不能被回收,那么它的依赖也不能被回收。这意味着serverData可能持有大量数据并且无法回收。在观察者的情况下,重要的是在不再需要它们时显式调用remove(或者需要将相关对象设置为不可访问)。这在过去尤为重要,因为某些浏览器(旧的IE6)不能很好地管理循环引用(有关更多信息,请参见下文)。今天,大多数浏览器可以并且将会在对象变得不可访问时回收观察者处理程序,即使没有明确删除监听器。但是,仍然建议在处理对象之前显式删除这些观察者。例如:varelement=document.getElementById('launch-button');varcounter=0;functiononClick(event){counter++;element.innerHtml='text'+counter;}element.addEventListener('click',onClick);//做一些事情element.removeEventListener('click',onClick);element.parentNode.removeChild(element);//当元素被销毁时//即使在旧浏览器中,元素和事件也会被回收现在的浏览器(包括IE和Edge)使用现代垃圾回收算法,可以找到并处理这些循环引用立即。换句话说,并不一定要先调用removeEventListener再删除节点。jQuery等框架和插件会在丢弃节点之前移除侦听器。这都是在内部处理的,以确保没有内存泄漏,即使在有问题的浏览器(是的,IE6)上也是如此。3:闭包闭包是JavaScript开发的一个关键方面:使用来自外部(封闭)函数的变量的内部函数。由于JavaScript运行时实现的差异,可能会通过以下方式导致内存泄漏:安慰。log("hi");};theThing={longStr:newArray(1000000).join('*'),someMethod:function(){console.log("message");}};};setInterval(replaceThing,1000);这段代码做了一件事:每次调用ReplaceThing时,theThing都会获得一个包含大数组和新闭包(someMethod)的对象。同时,变量unused持有一个引用originalThing的闭包(theThing是上次调用replaceThing产生的值)。已经有点糊涂了?最重要的是,一旦为同一父作用域中的作用域创建了闭包,该作用域就被共享了。在这里,作用域创建了一个闭包,并且someMethod和unused共享这个闭包内的内存。unused指的是originalThing。虽然unused不会被使用,但是someMethod可以使用theThing来使用replaceThing范围之外的变量(比如一些全局的)。someMethod和unused也有一个共同的闭包范围,unused对originalThing的引用强制originalThing保持活动状态(两个闭包共享整个范围)。这可以防止它被回收。当重复执行这段代码时,可以观察到使用的内存在不断增加。当垃圾收集运行时,它们也不会变小。本质上,已经创建了一个闭包链表(以theThing变量为根),并且这些闭包的每个作用域都间接引用了大型数组,从而导致大量内存泄漏。这个问题是由Meteor团队发现的,他们有一篇非常好的文章详细描述了闭包。4:DOM外部引用有时候在数据结构中存储DOM节点是很有用的,比如你想快速更新一个表的几行内容。此时,将对每一行的DOM节点的引用存储在字典或数组中是有意义的。此时一个DOM节点有两个引用:一个在dom树中,一个在字典中。如果将来某个时候您想要删除这些行,则需要确保这两个引用都不可访问。varelements={button:document.getElementById('button'),image:document.getElementById('image')};functiondoStuff(){image.src='http://example.com/image_name.png';}functionremoveImage(){//image是body元素的子节点document.body.removeChild(document.getElementById('image'));//此时我们在全局elements对象中还有#button的引用。//也就是说,buttom元素还在内存中,无法回收。当谈到DOM树内部或子节点时,需要考虑额外的注意事项。例如,您在JavaScript中保留对表格特定单元格的引用。有一天,您决定从DOM中删除表格,但保留对单元格的引用。人们可能会认为除了细胞之外的所有东西都会被回收。实际上并非如此:单元格是表的子节点,子节点保留对父节点的引用。具体来说,JS代码中的单元格引用会导致整个表格保留在内存中,因此在删除引用节点时要小心。我们SessionStack努力遵循这些最佳实践,因为:一旦您将sessionStack集成到您的生产应用程序中,它就会开始记录所有内容:DOM更改、用户交互、JS异常、堆栈跟踪、失败的网络请求、调试信息等。使用SessionStack,您可以在您的应用程序中重放问题并查看它们如何影响用户。所有这些都不会影响您的应用程序的性能。因为用户可以重新加载页面或在应用程序中跳转,所以必须妥善处理所有观察者、拦截器和变量赋值。以免造成内存泄漏,同时也防止增加整个应用程序的内存占用。这是一个免费计划,您可以立即试用。
