宝哥软件园

JavaScript内存泄漏的处理方法

编辑:宝哥软件园 来源:互联网 时间:2021-09-03

以下是JS遇到内存泄漏时的一系列处理方法。

随着编程语言功能的日益成熟和复杂,内存管理很容易被忽视。本文将讨论JavaScript中的内存泄漏以及如何处理,让大家在使用JavaScript编码时,能够更好地处理内存泄漏带来的问题。

摘要

像C这样的编程语言具有简单的内存管理功能,例如malloc()和free()。开发人员可以使用这些函数显式分配和释放系统内存。

创建对象和字符串后,JavaScript会分配内存,并在不再使用时自动释放内存。这种机制被称为垃圾收集。这种资源的释放看似是“自动”的,但其本质是混乱的,这给了JavaScript(以及其他高级语言)的开发人员一种不关心内存管理的错误印象。其实这是一个很大的错误。

即使使用高级语言,开发人员也应该了解内存管理的知识。有时自动内存管理会出现问题(例如垃圾收集器中的错误或实现限制等)。),开发人员必须了解这些问题,才能正确处理。

记忆生命周期

无论您使用什么编程语言,内存生命周期几乎都是一样的:

以下是内存生命周期中每一步的概述:

分配内存——内存由操作系统分配,允许程序使用。在简单的编程语言中,这个过程是开发人员应该处理的显式操作。但是,在高级编程语言中,系统将帮助您完成此操作。内存使用——这是程序在使用之前申请内存的时间段,您的代码将使用分配的变量

读写内存。

空闲内存-不再需要的空闲内存,以确保它变得空闲并可以再次使用。像分配内存一样,这个操作需要用简单的编程语言来显示。什么是记忆?

在硬件层面,计算机内存由大量触发器组成。每个触发器包含一些晶体管,可以存储一位数据。单个触发器可以通过唯一的标识符来寻址,因此我们可以读取并覆盖它们。因此,从概念上讲,我们可以把整个计算机内存看作是一个可以读写的大空间。

许多东西都存储在内存中:

程序使用的所有变量和其他数据。程序代码,包括操作系统代码。

编译器和操作系统一起工作来处理大部分内存管理,但是我们需要了解本质上发生了什么。

编译代码时,编译器检查原始数据类型,预先计算它们需要多少内存,然后将所需内存分配给调用堆栈空间中的程序。分配这些变量的空间称为堆栈空间,通过函数的调用,内存将被添加到现有的内存中。终止时,按后进先出顺序删除空格。例如,以下语句:

int n;//4字节int x[4];//由4个元素组成的数组,每个元素占用4个字节的double m;//8字节编译器插入与操作系统交互的代码,以请求堆栈中存储变量所需的字节数。

在上面的例子中,编译器知道每个变量的确切内存地址。事实上,每当我们写这个变量n时,它都会被内部翻译为“内存地址4127963”。

请注意,如果我们尝试访问x[4],我们将访问与m相关联的数据。这是因为我们正在访问数组中不存在的元素-它比数组中最后一个数据实际分配的元素多4字节x[3],并且最终可能读取(或覆盖)一些m位。这将对其他人产生不利影响。

当一个函数调用其他函数时,每个函数在被调用时都会得到自己的堆栈块。它保存所有的局部变量和一个程序计数器,并记录在哪里执行。当该功能完成时,其内存块将被释放,并可再次用于其他目的。

动态分配

如果我们不知道编译时需要多少内存变量,事情就会变得复杂。假设我们想要执行以下操作:

int n=readInput();//读取用户输入.//创建一个包含“n”个元素的数组。编译时,编译器不知道数组需要多少内存,因为它是由用户提供的输入值决定的。

因此,它不能在堆栈上为变量分配空间。相反,我们的程序需要在运行时向操作系统明确请求适当的空间。这个内存是从堆空间中分配的。下表总结了静态和动态内存分配之间的差异:

在JavaScript中分配内存

现在解释如何在JavaScript中分配内存。

JavaScript将开发人员从处理内存分配中解放出来。

var n=374//为numbervar s='sessionstack '分配内存;//为stringvar o={a: 1,b: null}分配内存;//为对象及其包含的值分配内存svar a=[1,null,' str '];//(like object)为//数组及其包含的值分配内存函数f(a){返回a3;} //分配一个函数(它是一个可调用的对象)//函数表达式也分配一个objectsomelement . addeventlistener(' click '),function(){ somelement . style . background color=' blue ';},false);一些函数调用也会导致对象分配:

var d=新日期();//分配一个Date object var e=document . create element(' div ');//分配domelement方法可以分配新的值或对象:

var s1=' sessionstackvar s2=s1.substr(0,3);//s2是新字符串//由于字符串是不可变的,//JavaScript可能会决定不分配内存,//而只是存储[0,3]范围. var a1=['str1 ',' str 2 '];var a2=['str3 ',' str 4 '];var a3=a1 . concat(a2);//包含4个元素的新数组//a1和a2元素的串联使用了JavaScript中的内存

基本上,在JavaScript中使用分配的内存意味着在内存中读写。

这可以通过读取或写入变量或对象属性的值,甚至将参数传递给函数来实现。

当不再需要时释放内存

大多数内存泄漏发生在这个阶段,这个阶段最困难的问题是确定分配的内存何时不再需要。它通常需要开发人员确定程序的哪个部分不再需要这些内存,并释放它。

高级语言嵌入了一个叫做垃圾收集器的函数,它的工作是跟踪内存的分配和使用情况,以便在不再需要分配内存时自动释放内存。

遗憾的是,这个过程不能那么精确,因为一些不再需要的内存等问题无法通过算法解决。

大多数垃圾收集器通过收集无法访问的内存来工作,例如,当指向它的变量超出范围时。但是,这种方法只能收集内存空间的近似值,因为在内存的某些地方可能仍然有指向它的变量,但不会被再次访问。

因为确定某些内存是否“不再需要”是不可判定的,所以垃圾收集机制有一定的局限性。下面将解释主要垃圾收集算法的概念及其局限性。

内存引用

垃圾收集算法依赖的主要概念之一是内存引用。

在内存管理的情况下,如果一个对象访问一个变量(可以是隐式的,也可以是显式的),就说这个对象引用了另一个对象。例如,一个JavaScript对象具有对其原始对象的引用(隐式引用)及其属性值(显式引用)。

在这种情况下,“对象”的概念扩展到了比普通JavaScript对象更广的范围,也包括了函数范围。

垃圾收集引用计数

这是最简单的垃圾收集算法。如果对它的引用为零,则该对象被视为垃圾回收。

请看下面的代码:

var o1={ o2: { x: 1 } }//创建了2个对象。//“O2”被“o1”对象作为其属性之一引用。//无可以进行垃圾收集-垃圾回收o3=o1//第二件事是“o3”变量//引用了“o1”所指向的对象。O1=1;//现在,最初在“o1”中的对象具有//单个引用,由“O3”variable var O4=O3 . O2体现;//对对象的“o2”属性的引用。//这个对象现在有两个引用:一个作为//属性。//另一个为‘O4’variable O3=‘374’;//最初在“o1”中的对象现在没有//引用。//可以垃圾回收。//但是,它的“o2”属性仍然//被“o4”变量引用,因此不能//freed . O4=null;//原来在/“O1”中的对象的“o2”属性没有任何引用。//可以垃圾收集。

循环导致问题

周期时间是有限制的。例如,以下示例创建两个对象并相互引用,从而创建循环引用。函数被调用后,它们会超出范围,所以它们实际上是无用的,可以释放。然而,引用计数算法认为,由于这两个对象中的每一个都至少被引用一次,所以它们都不能被垃圾收集机制回收。

函数f(){ var O1={ };var O2={ };o1.p=o2//o1引用o2o 2 . p=O1;//o2参考o1。这就形成了一个循环。} f();

标记和扫描算法

为了决定是否需要一个对象,标记和扫描算法确定该对象是否是活动的。

标记和扫描算法经过以下三个步骤:

根:通常,根是代码中引用的全局变量。例如,在JavaScript中,可以作为根的全局变量是一个“窗口”对象。Node.js中相同的对象称为“全局”。垃圾收集器建立了所有根的完整列表。然后,该算法检查所有根及其子对象,并将它们标记为活动的(也就是说,它们不是垃圾)。根用户无法访问的任何内容都将被标记为垃圾。最后,垃圾收集器释放所有未标记为活动的内存块,并将内存返回给操作系统。

该算法优于引用计数垃圾收集算法。在JavaScript垃圾收集领域(代码/增量/并发/并行垃圾收集)所做的所有改进都是对标记和扫描算法的改进,而不是对垃圾收集算法本身的改进。

周期不再是一个问题

在上面的交叉引用示例中,函数调用返回后,这两个对象不再被全局对象可访问的对象引用。因此,它们将被垃圾收集器发现,以便可以回收。

即使对象之间有引用,也不能从根目录访问,因此它们将作为垃圾收集。

抵制垃圾收集器的直观行为

虽然垃圾收集器很容易使用,但它们也有自己的一套标准,其中之一就是不确定性。换句话说,垃圾收集是不可预测的。你不能真正知道什么时候收集,这意味着在某些情况下,程序会使用更多的内存,尽管这实际上是需要的。在其他情况下,在特别敏感的应用程序中,可能会出现短暂的暂停。虽然不确定性意味着无法确定何时收集,但大多数垃圾收集都在分配期间实现了共享收集的常见模式。如果不执行分配,大多数垃圾收集将保持空闲。在以下情况下:

执行大量分配。这些元素中的大部分(或全部)被标记为不可访问(假设我们指向一个不再需要的缓存的引用)。没有要执行的进一步分配。

在这种情况下,大多数垃圾收集不会做任何收集工作。换句话说,即使有不可用的引用要收集,收集器也不会收集它们。虽然这不是严格的泄漏,但它仍然会导致比平时更高的内存使用率。

什么是内存泄漏?

内存泄漏是应用程序使用的内存碎片,当不再需要时,无法返回到操作系统或可用内存池。

编程语言有不同的内存管理方式。然而,是否使用某一段内存实际上是一个不可决定的问题。换句话说,只有开发人员确切地知道一块内存是否需要返回给操作系统。

四种常见的JavaScript内存泄漏

1.全局变量

JavaScript以一种有趣的方式处理未声明的变量:当引用未声明的变量时,会在全局对象中创建一个新变量。在浏览器中,全局对象将是窗口,这意味着

函数foo(arg){ bar=' some text ';}相当于:

函数foo(arg){ window . bar=' some text ';}bar只是foo函数中引用的一个变量。如果不使用var声明,将创建一个额外的全局变量。在上述情况下,不会造成很大的问题。但是,如果是这样的话。

您也可能意外地创建了一个全局变量:

function foo() {this.var1='潜在意外全局';}//Foo自己调用,这指向全局对象(window)//而不是undefined . Foo();您可以在JavaScript文件的开头添加“使用严格”。为了避免这种错误,这种方式将打开严格解析的JavaScript模式,从而防止意外创建全局变量。

意外的全局变量当然是个问题。更常见的情况是,您的代码会受到显式全局变量的影响,这些变量不能在垃圾收集器中收集。需要特别注意用于临时存储和处理大量信息的全局变量。如果必须使用全局变量来存储数据,请确保将它们指定为空值,或者在完成后重新指定它们。

2.忘记计时器或回拨

下面是setInterval的一个例子,它经常在JavaScript中使用。

对于提供监视和其他接受回调的工具的库,确保回调的所有引用在其实例不可访问时变得不可访问是很常见的。但是下面的代码是一个例外:

var server data=load data();setInterval(function(){ var renderer=document . getelementbyid(' renderer ');if(渲染器){ renderer . innerhtml=JSON . stringify(ServerDATa);}}, 5000);//这将每隔~ 5秒执行一次。上面的代码片段显示了使用引用不再需要的节点或数据的计时器的结果。

渲染器对象可能会在某个时候被替换或删除,这将使间隔处理程序封装的块变得多余。如果发生这种情况,处理程序及其依赖项都不会被收集,因为需要先停止间隔。所有这些都归结为一个原因,用于存储和处理负载数据的服务器数据将不会被收集。

使用监视器时,您需要确保进行显式调用来删除它们。

幸运的是,大多数现代浏览器都会为您这样做:即使您忘记删除侦听器,当被监控对象变得不可访问时,它们也会自动收集监控处理器。这是一些浏览器过去无法处理的情况(比如旧的IE6)。

请看下面的例子:

var元素=document . getelementbyid(' launch-button ');var计数器=0;函数onClick(事件){ counterelement.innerHtml='text '计数器;} element . addeventlistener(' click ',OnClick);//Do stuff element . removeeventlistener(' click ',onClick);element.parentNode.removeChild(元素);//现在当元素超出范围时,//元素和onclick都将被收集,即使在不能很好地处理循环的旧浏览器中也是如此//。因为现代浏览器支持垃圾收集机制,当一个节点变得不可访问时,就不再需要调用removeEventListener,因为垃圾收集机制会正确处理这些节点。

如果使用的是jQueryAPI(其他库和框架都支持),也可以在使用节点之前删除侦听器。即使应用程序在较旧的浏览器版本下运行,库也能确保没有内存泄漏。

3.关闭

JavaScript开发的一个关键方面是闭包。闭包是一个内部函数,可以访问外部(封闭)函数的变量。由于JavaScript运行时的实现细节,可能存在以下形式的内存泄漏:

var theThing=nullvar replace thing=function(){ var original thing=ThEring;varunused=function(){ if(original thing)//对' original thing' console.log ("hi ")的引用;};theThing={ longStr:新数组(1000000)。join('* '),some method:function(){ console . log(" message ");} };};setInterval(replaceThing,1000);一旦调用了replaceThing,Thing将获得一个由一个大数组和一个新闭包(someMethod)组成的新对象。然而,originalThing将被未使用的变量所保持的闭包所引用(这是Thing replaceThing从前面的调用变量中得到的东西)。应该记住,一旦在同一个父作用域中为闭包创建了闭包的作用域,这个作用域就被共享了。

在这种情况下,闭包创建的作用域将与未使用的方法共享。但是,未使用的具有原始引用。即使从未使用过未使用的东西,有些方法也可以通过Thing在整个范围之外使用replaceThing。此外,someMethod通过unused来共享闭包的范围,unused必须引用originalThing来保持其他闭包的活动(两个闭包之间的整个共享范围)。这防止了它被收集。

所有这些都会导致相当大的内存泄漏。当上面的代码片段一遍又一遍地运行时,您会看到内存使用量在上升。当垃圾收集器运行时,它的内存大小不会缩小。在这种情况下,创建了一个闭包的链表,每个闭包作用域都间接引用了一个大数组。

4:超出了DOM引用

在某些情况下,开发人员会将DOM节点存储在数据结构中,例如,如果您想快速更新表中的几行。如果对每个DOM行的引用存储在字典或数组中,那么对同一个DOM元素将有两个引用:一个在DOM树中,另一个在字典中。如果不再需要这些行,则需要使这两个引用都不可访问。

var elements={ button : document . getelementbyid(' button '),image : document . getelementbyid(' image ')};function Dostuff(){ elements . image . src=' http://example.com/image _ name . png ';}函数removeImage() {//该图像是body元素. document . body . remove child(document . getelementbyid(' image '))的直接子级;//此时,我们在//global elements对象中仍然有对#button的引用。换句话说,按钮元素//仍然在内存中,GC无法收集。}说到DOM树中的内部节点或叶节点,还有一个额外的因素需要考虑。如果您在代码中保留对表单元格(标记)的引用,并决定从DOM中删除该表,并且您需要保留对该特定单元格的引用,则可能会出现严重的内存泄漏。您可能认为垃圾收集器会释放除该单元之外的所有内容,但事实并非如此。因为单元格是表的子节点,并且子节点保留对父节点的引用,所以对表单元格的引用会将整个表保存在内存中。

摘要

以上内容是对JavaScript内存管理机制的讲解和四种常见内存泄漏的分析。希望对JavaScript程序员有所帮助。

更多资讯
游戏推荐
更多+