宝哥软件园

中高层前端必须了解的JS中内存管理(推荐)

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

前言

像C这样的低级语言一般都有低级的内存管理接口,比如malloc()和free(),用来分配和释放内存。

至于JavaScript,它在创建变量(对象、字符串等)时分配内存。)并在不再使用时“自动”释放内存。这种自动释放内存的过程称为垃圾回收。

由于自动垃圾收集机制的存在,大多数Javascript开发人员都觉得自己可以不关心内存管理,这在某些情况下会导致内存泄漏。

记忆生命周期

JS环境下分配的内存有以下声明周期:1。内存分配:当我们声明变量、函数和对象时,系统会自动为它们分配内存。2.内存使用:即读写内存,即使用变量、函数等。3.内存回收:使用后,垃圾回收机制会自动回收不再使用的内存。

JS的内存分配

为了不麻烦程序员分配内存,JavaScript在定义变量时完成了内存分配。

var n=123//为数值变量var s='azerty '分配内存;//将内存分配给字符串var o={a: 1,b : null };//为对象及其包含的值分配内存//为数组及其包含的值分配内存(就像对象一样)var a=[1,null,' abra '];函数f(a){返回a2;}//分配内存给函数(可调用对象)//函数表达式也可以分配对象someelement。addeventlistener ('click ',function () {someelement。风格。backgroundcolor=' blue},false);一些函数调用导致分配对象内存:

var d=新日期();//分配一个Date对象var e=document . create element(' div ');//分配DOM元素有一些方法可以分配新变量或新对象:

var s=' azertyvar s2=s.substr(0,3);//s2是一个新字符串。//因为字符串是不变的,//JavaScript可能会决定不分配内存。//它只存储[0-3]的范围。var a=['ouais ouais ',' nan nan '];var a2=['generation ',' nan nan '];var a3=a . concat(a2);//新数组有四个元素,这是A连接a2导致JS的内存使用情况

使用值的过程实际上是读写分配的内存。

读取和写入可能是写入变量或对象的属性值,甚至是传递函数的参数。

var a=10//分配内存console . log(a);//内存的使用。JS的内存回收

JS有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?其实很简单,就是找出不再使用的值,然后释放它们占用的内存。

大多数内存管理问题都在这个阶段。这里最难的任务是找到不再需要的变量。

不再需要的变量,即生命周期结束时的变量,是局部变量。局部变量只存在于函数执行期间。当函数完成并且没有其他引用(闭包)时,变量将被标记为回收。

在浏览器卸载页面之前,全局变量的生命周期不会结束,这意味着全局变量不会被视为垃圾收集。

由于自动垃圾收集机制的存在,开发者可以不关心或不关注内存释放的相关问题,但无用内存的释放是客观存在的。不幸的是,即使不考虑垃圾收集对性能的影响,最新的垃圾收集算法也不能智能地收集所有极端情况。

接下来,我们来探讨JS垃圾收集的机制。

碎片帐集

引用

垃圾收集算法主要依靠引用的概念。

在内存管理环境中,如果一个对象可以访问另一个对象(隐式或显式),它被称为引用另一个对象的对象。

例如,一个Javascript对象有一个对其原型的引用(隐式引用)和一个对其属性的引用(显式引用)。

这里“对象”的概念不仅指JavaScript对象,还包括函数范围(或全局词法范围)。

垃圾收集引用计数

这是主要的垃圾收集算法。

引用计数算法定义了“内存不再使用”的标准,即一个对象是否有引用。

如果没有其他对象指向它,则意味着不再需要该对象。

var o={ a: { b:2 } }//创建两个对象,一个作为另一个的属性引用,另一个赋给变量o//显然,它们都不能被垃圾回收。var O2=o;//o2变量是对“这个对象”的第二次引用,o=1;//现在把“这个对象”原来的引用O替换为o2。var oa=o2.a//引用“this object”的A属性//现在,“this object”有两个引用,一个是o2,一个是oao2=' yo//原始对象现在被零引用。//可以垃圾收集。//但是其属性A的对象仍然被oa引用,所以oa=null还不能采集。//a属性的对象现在是零引用。//可以垃圾收集。从上面可以看出,参考计数算法是一种简单有效的算法。但它有一个致命的问题:循环引用。

如果两个对象相互引用,即使它们不再使用,垃圾收集也不会回收,从而导致内存泄漏。

让我们看一个循环引用的例子:

函数f(){ var o={ };var O2={ };o.a=o2//o引用O2 O2 . a=o;//这里o2指的是return ' azerty} f();上面我们声明了一个函数f,它包含两个相互引用的对象。

调用函数后,对象o1和o2实际上已经离开了函数的范围,因此不再需要它们。

但是根据引用计数的原理,它们之间的相互引用仍然存在,所以这部分内存不会被回收,内存泄漏是不可避免的。

让我们看一个实际的例子:

var div=document . create element(' div ');div . onclick=function(){ console . log(' click ');};上面的JS编写非常常见,创建一个DOM元素并绑定一个click事件。

此时变量div有对事件处理程序的引用,事件处理程序也有对div的引用!(div变量可以在函数中访问。).

出现了顺序引用,根据上面提到的算法,这部分内存不可避免地会泄露。

为了解决循环引用带来的问题,现代浏览器使用标签移除算法实现垃圾收集。

标记去除算法

标记移除算法将“不再使用的对象”定义为“不可到达的对象”。简单来说就是从根(JS中的全局对象)开始,定期扫描内存中的对象。任何能从根上触及到的东西仍然是需要的。那些无法从根访问的对象将被标记为不再使用,稍后将被回收。

从这个概念可以看出,不可碰对象包括无引用对象的概念(无任何引用的对象也是不可碰对象)。但事实可能并非如此。

工作流程:1。垃圾收集器将在运行时标记存储在内存中的所有变量。2.从根开始,去除可以触摸的对象的标记。3.那些仍然被标记的变量被视为要删除的变量。4.最后,垃圾收集器将执行内存清除的最后一步,销毁那些标记的值并回收它们占用的内存空间。

循环引用不再是问题

看看前面循环引用的例子:

函数f(){ var o={ };var O2={ };o.a=o2//o引用O2 O2 . a=o;//o2指o return ' azerty} f();函数调用返回后,两个循环引用的对象在垃圾收集过程中不能再从全局对象中获取引用。因此,它们将被垃圾收集器回收。

内存泄漏

什么是内存泄漏

程序需要内存才能运行。只要程序请求它,操作系统或运行时就必须提供内存。

对于连续运行的服务进程(守护进程),必须及时释放不再使用的内存。否则,内存占用越来越高,不仅会影响系统性能,还会导致进程崩溃。

本质上,内存泄漏是由于疏忽或错误导致程序无法释放不再使用的内存而造成的内存浪费。

一种识别内存泄漏的方法

经验法则是,如果连续五次垃圾回收后,每次内存占用都比较大,就会出现内存泄漏。

这需要实时查看内存使用情况。

在Chrome浏览器中,我们可以通过这种方式查看内存使用情况。1.打开开发工具并选择性能面板。2.检查顶部的内存。3.单击左上角的录制按钮。4.在页面上执行各种操作来模拟用户的使用情况。5.一段时间后,单击对话框中的停止按钮,此时的内存使用情况将显示在面板上

让我们看一下效果图:

目前判断是否存在内存泄漏有两种方法:1。几次快照后,比较每个快照中的内存占用情况,如果呈现上升趋势,则可以认为存在内存泄漏;2.快照后,查看内存占用的当前趋势图,如果趋势不稳定且向上,则可以认为存在内存泄漏。

使用节点提供的process.memoryUsage方法检查服务器环境中的内存状况

console . log(process . memoryusage());//{//rss: 27709440,//heaptotal3360 5685248,//heapused: 349392,//external3360 8772//}进程。memoryusage返回一个包含节点进程内存使用信息的对象。

这个对象包含四个字段,单位是字节,含义如下:

Rss(常驻集大小):所有内存占用,包括指令区和堆栈。HeapTotal:由“堆”占用的内存,包括已使用和未使用的内存。堆:堆中被使用的部分。外部:V8引擎内部C对象占用的内存。基于heapUsed字段判断内存泄漏。

常见的内存泄漏案例

意外的全局变量

function foo(){ bar1=' some text ';//没有声明该变量实际上是一个全局变量=window . bar 1 this . bar 2=' sometext '/global variable=window . bar 2 } foo();在本例中,意外创建了两个全局变量bar1和bar2

忘记计时器和回调函数

在许多库中,如果使用观察者模式,则提供回调方法来调用一些回调函数。

记得回收这些回调函数。举一个setInterval的例子:

var server data=load data();setInterval(function(){ var renderer=document . getelementbyid(' renderer ');if(渲染器){ renderer . innerhtml=JSON . stringify(ServerDATa);}}, 5000);//每5秒调用一次。如果移除后续的渲染器元素,整个计时器实际上没有任何效果。

但是如果不回收计时器,整个计时器仍然有效。计时器不仅不能被内存回收,

计时器函数中的依赖关系无法回收。服务器在这种情况下,数据也不能回收。

关闭

在JS的开发中,我们经常使用闭包,闭包是内部函数,可以访问包含它们的外部函数中的变量。

在以下情况下,闭包也会导致内存泄漏:

var theThing=nullvar replace thing=function(){ var original thing=ThEring;varunused=function(){ if(original thing)//对' original thing' console.log('hi ')的引用;};Thing={ longStr:新数组(1000000)。join('* '),some method : function(){ console . log(' message ');} };};setInterval(replaceThing,1000);在这段代码中,每次调用replaceThing时,Thing都会获得一个对象,该对象包含一个巨大的数组和一个新闭包的someMethod。同时,unused是指originalThing的闭包。

这个例子的关键点是闭包之间有范围共享。虽然可能永远不会调用未使用的方法,但可能会调用某些方法,这将使其无法回收内存。

当重复执行这段代码时,内存将继续增长。

DOM引用

很多时候,我们在操作Dom的时候,会把Dom的引用保存在数组或者Map中。

var elements={ image : document . getelementbyid(' image ')};function Dostuff(){ elements . image . src=' http://example.com/image _ name . png ';}函数remove image(){ document . body . remove child(document . getelementbyid(' image '));//此时我们还有对#Image的引用,图像元素在内存中无法回收。}在上面的例子中,即使我们移除了图像元素,仍然有对图像元素的引用,这对于内存回收来说仍然无法对齐。

需要注意的另一点是对Dom树的叶节点的引用。

例如,如果我们在一个表中引用td元素,一旦整个表在Dom中被删除,我们直观地感觉到,内存回收应该回收引用的td之外的其他元素。

但实际上,这个td元素是整个表的子元素,对其父元素的引用是保留的。

因此,无法回收整个表。因此,我们应该小心对Dom元素的引用。

如何避免内存泄漏

记住一个原则:及时归还不用的东西。1.减少不必要的全局变量,使用严格模式,避免意外创建全局变量。2.使用完数据后,及时取消引用(闭包中的变量、dom引用、计时器被清除)。3.整理好自己的逻辑,避免无限循环带来的浏览器堵塞和崩溃的问题。

以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。

更多资讯
游戏推荐
更多+