APP下载

JavaScript基础:如何管理JS中的内存?什么是内存泄漏和垃圾回收

消息来源:baojiabao.com 作者: 发布时间:2024-05-04

报价宝综合消息JavaScript基础:如何管理JS中的内存?什么是内存泄漏和垃圾回收

连结:https://juejin.im/post/5d0706a6f265da1bc23f77a9

前言

像C语言这样的底层语言一般都有底层的内存管理界面,比如 malloc()和free()用于分配内存和释放内存。 而对于JavaScript来说,会在建立变数(物件,字串等)时分配内存,并且在不再使用它们时“自动”释放内存,这个自动释放内存的过程称为垃圾回收。 因为自动垃圾回收机制的存在,让大多Javascript开发者感觉他们可以不关心内存管理,所以会在一些情况下导致内存泄漏。

内存生命周期

JS 环境中分配的内存有如下宣告周期:

内存分配:当我们申明变数、函式、物件的时候,系统会自动为他们分配内存内存使用:即读写内存,也就是使用变数、函式等内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存JS 的内存分配

为了不让程序员费心分配内存,JavaScript 在定义变数时就完成了内存分配。

有些函式呼叫结果是分配物件内存:

var d = new Date(); // 分配一个 Date 物件

var e = document.createElement('div'); // 分配一个 DOM 元素

有些方法分配新变数或者新物件:

var s = "azerty";

var 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 物件,还包括函式作用域(或者全域性词法作用域)。

引用计数垃圾收集

这是最初级的垃圾回收算法。

引用计数算法定义“内存不再使用”的标准很简单,就是看一个物件是否有指向它的引用。 如果没有其他物件指向它了,说明该物件已经不再需了。

由上面可以看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用

如果两个物件相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露。

来看一个循环引用的例子:

上面我们申明了一个函式 f ,其中包含两个相互引用的物件。 在呼叫函式结束后,物件 o1 和 o2 实际上已离开函式范围,因此不再需要了。 但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免了。

再来看一个实际的例子:

var div = document.createElement("div");

div.onclick = function() {

console.log("click");

};

上面这种JS写法再普通不过了,建立一个DOM元素并系结一个点选事件。 此时变数 div 有事件处理函式的引用,同时事件处理函式也有div的引用!(div变数可在函式内被访问)。 一个循序引用出现了,按上面所讲的算法,该部分内存无可避免的泄露了。

为了解决循环引用造成的问题,现代浏览器通过使用标记清除算法来实现垃圾回收。

标记清除算法

标记清除算法将“不再使用的物件”定义为“无法达到的物件”。 简单来说,就是从根部(在JS中就是全域性物件)出发定时扫描内存中的物件。 凡是能从根部到达的物件,都是还需要使用的。 那些无法由根部出发触及到的物件被标记为不再使用,稍后进行回收。

从这个概念可以看出,无法触及的物件包含了没有引用的物件这个概念(没有任何引用的物件也是无法触及的物件)。 但反之未必成立。

工作流程:

垃圾收集器会在执行的时候会给储存在内存中的所有变数都加上标记。从根部出发将能触及到的物件的标记清除。那些还存在标记的变数被视为准备删除的变数。最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

循环引用不再是问题了

再看之前循环引用的例子:

function f(){

var o = {};

var o2 = {};

o.a = o2; // o 引用 o2

o2.a = o; // o2 引用 o

return "azerty";

}

f();

函式呼叫返回之后,两个循环引用的物件在垃圾收集时从全域性物件出发无法再获取他们的引用。 因此,他们将会被垃圾回收器回收。

内存泄漏

什么是内存泄漏

程式的执行需要内存。只要程式提出要求,操作系统或者执行时(runtime)就必须供给内存。

对于持续执行的服务程序(daemon),必须及时释放不再用到的内存。 否则,内存占用越来越高,轻则影响系统性能,重则导致程序崩溃。

本质上讲,内存泄漏就是由于疏忽或错误造成程式未能释放那些已经不再使用的内存,造成内存的浪费。

内存泄漏的识别方法

经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。 这就要求实时检视内存的占用情况。

在 Chrome 浏览器中,我们可以这样检视内存占用情况

开启开发者工具,选择 Performance 面板在顶部勾选 Memory点选左上角的 record 按钮在页面上进行各种操作,模拟使用者的使用情况一段时间后,点选对话方块的 stop 按钮,面板上就会显示这段时间的内存占用情况来看一张效果图:

我们有两种方式来判定当前是否有内存泄漏:

多次快照后,比较每次快照中内存的占用情况,如果呈上升趋势,那么可以认为存在内存泄漏某次快照后,看当前内存占用的趋势图,如果走势不平稳,呈上升趋势,那么可以认为存在内存泄漏在服务器环境中使用 Node 提供的 process.memoryUsage 方法检视内存情况

console.log(process.memoryUsage());

// {

// rss: 27709440,

// heapTotal: 5685248,

// heapUsed: 3449392,

// external: 8772

// }

process.memoryUsage返回一个物件,包含了 Node 程序的内存占用资讯。

该物件包含四个字段,单位是字节,含义如下:

rss(resident set size):所有内存占用,包括指令区和堆叠。heapTotal:"堆"占用的内存,包括用到的和没用到的。heapUsed:用到的堆的部分。external: V8 引擎内部的 C++ 物件占用的内存。判断内存泄漏,以heapUsed字段为准。

常见的内存泄露案例

意外的全域性变数

function foo() {

bar1 = 'some text'; // 没有宣告变数 实际上是全域性变数 => window.bar1

this.bar2 = 'some text' // 全域性变数 => window.bar2

}

foo();

在这个例子中,意外的建立了两个全域性变数 bar1 和 bar2

被遗忘的定时器和回拨函式

在很多库中, 如果使用了观察者模式, 都会提供回拨方法, 来呼叫一些回拨函式。 要记得回收这些回拨函式。举一个 setInterval的例子:

var serverData = loadData();

setInterval(function() {

var renderer = document.getElementById('renderer');

if(renderer) {

renderer.innerHTML = JSON.stringify(serverData);

}

}, 5000); // 每 5 秒呼叫一次

如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函式中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。

闭包

在 JS 开发中,我们会经常用到闭包,一个内部函式,有权访问包含其的外部函式中的变数。 下面这种情况下,闭包也会造成内存泄露:

这段程式码,每次呼叫 replaceThing 时,theThing 获得了包含一个巨大的阵列和一个对于新闭包 someMethod 的物件。 同时 unused 是一个引用了 originalThing 的闭包。

这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被呼叫,但是 someMethod 可能会被呼叫,就会导致无法对其内存进行回收。 当这段程式码被反复执行时,内存会持续增长。

DOM 引用

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

上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。

另外需要注意的一个点是,对于一个 Dom 树的叶子节点的引用。 举个例子: 如果我们引用了一个表格中的td元素,一旦在 Dom 中删除了整个表格,我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素。 但是事实上,这个 td 元素是整个表格的一个子元素,并保留对于其父元素的引用。 这就会导致对于整个表格,都无法进行内存回收。所以我们要小心处理对于 Dom 元素的引用。

如何避免内存泄漏

记住一个原则:不用的东西,及时归还

减少不必要的全域性变数,使用严格模式避免意外建立全域性变数。在你使用完资料后,及时解除引用(闭包中的变数,dom引用,定时器清除)。组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

2019-10-28 23:57:00

相关文章