前言
像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"]; function f(a){ return a + 2; } // 给函数(可调用的对象)分配内存 // 函数表达式也能分配一个对象 someElement.addEventListener('click', function(){ someElement.style.backgroundColor = 'blue'; }, false);
有些函数调用结果是分配对象内存:
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 对象,还包括函数作用域(或者全局词法作用域)。
引用计数垃圾收集
这是最初级的垃圾回收算法。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。
如果没有其他对象指向它了,说明该对象已经不再需了。
var o = { a: { b:2 } }; // 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o // 很显然,没有一个可以被垃圾收集 var o2 = o; // o2变量是第二个对“这个对象”的引用 o = 1; // 现在,“这个对象”的原始引用o被o2替换了 var oa = o2.a; // 引用“这个对象”的a属性 // 现在,“这个对象”有两个引用了,一个是o2,一个是oa o2 = "yo"; // 最初的对象现在已经是零引用了 // 他可以被垃圾回收了 // 然而它的属性a的对象还在被oa引用,所以还不能回收 oa = null; // a属性的那个对象现在也是零引用了 // 它可以被垃圾回收了
由上面可以看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。
如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露。
来看一个循环引用的例子:
function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o 这里 return "azerty"; } f();
上面我们申明了一个函数 f ,其中包含两个相互引用的对象。
在调用函数结束后,对象 o1 和 o2 实际上已离开函数范围,因此不再需要了。
但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免了。
再来看一个实际的例子:
var div = document.createElement("div"); div.onclick = function() { console.log("click"); };
上面这种JS写法再普通不过了,创建一个DOM元素并绑定一个点击事件。
此时变量 div 有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。
一个循序引用出现了,按上面所讲的算法,该部分内存无可避免的泄露了。
为了解决循环引用造成的问题,现代浏览器通过使用标记清除算法来实现垃圾回收。
标记清除算法
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。
凡是能从根部到达的对象,都是还需要使用的。
那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。
但反之未必成立。
工作流程:
1.垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
2.从根部出发将能触及到的对象的标记清除。
3.那些还存在标记的变量被视为准备删除的变量。
4.最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。
循环引用不再是问题了
再看之前循环引用的例子:
function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o return "azerty"; } f();
函数调用返回之后,两个循环引用的对象在垃圾收集时从全局对象出发无法再获取他们的引用。
因此,他们将会被垃圾回收器回收。
内存泄漏
什么是内存泄漏
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。
否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
本质上讲,内存泄漏就是由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费。
内存泄漏的识别方法
经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。
这就要求实时查看内存的占用情况。
在 Chrome 浏览器中,我们可以这样查看内存占用情况
1.打开开发者工具,选择 Performance 面板
2.在顶部勾选 Memory
3.点击左上角的 record 按钮
4.在页面上进行各种操作,模拟用户的使用情况
5.一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况
来看一张效果图:
我们有两种方式来判定当前是否有内存泄漏:
1.多次快照后,比较每次快照中内存的占用情况,如果呈上升趋势,那么可以认为存在内存泄漏
2.某次快照后,看当前内存占用的趋势图,如果走势不平稳,呈上升趋势,那么可以认为存在内存泄漏
在服务器环境中使用 Node 提供的 process.memoryUsage 方法查看内存情况
console.log(process.memoryUsage()); // { // rss: 27709440, // heapTotal: 5685248, // heapUsed: 3449392, // external: 8772 // }
process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。
该对象包含四个字段,单位是字节,含义如下:
判断内存泄漏,以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 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。
下面这种情况下,闭包也会造成内存泄露:
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // 对于 'originalThing'的引用 console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。
同时 unused 是一个引用了 originalThing 的闭包。
这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。
当这段代码被反复执行时,内存会持续增长。
DOM 引用
很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中。
var elements = { image: document.getElementById('image') }; function doStuff() { elements.image.src = 'http://example.com/image_name.png'; } function removeImage() { document.body.removeChild(document.getElementById('image')); // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收. }
上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。
另外需要注意的一个点是,对于一个 Dom 树的叶子节点的引用。
举个例子: 如果我们引用了一个表格中的td元素,一旦在 Dom 中删除了整个表格,我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素。
但是事实上,这个 td 元素是整个表格的一个子元素,并保留对于其父元素的引用。
这就会导致对于整个表格,都无法进行内存回收。所以我们要小心处理对于 Dom 元素的引用。
如何避免内存泄漏
记住一个原则:不用的东西,及时归还。
1.减少不必要的全局变量,使用严格模式避免意外创建全局变量。
2.在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
3.组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。
本文向大家介绍Python 必须了解的5种高级特征,包括了Python 必须了解的5种高级特征的使用技巧和注意事项,需要的朋友参考一下 Python 是一种美丽的语言,它简单易用却非常强大。但你真的会用 Python 的所有功能吗? 任何编程语言的高级特征通常都是通过大量的使用经验才发现的。比如你在编写一个复杂的项目,并在 stackoverflow 上寻找某个问题的答案。然后你突然发现了一个非常
问题内容: Java程序员知道JVM运行垃圾回收器,而System.gc()只是建议JVM运行垃圾回收器。如果我们使用System.gc(),并不一定会立即运行GC。 如果我误解了Java的垃圾收集器,请纠正我。 除了依赖Java的Garbage Collector之外,还有其他方法可以进行内存管理吗? 如果您打算通过某种有助于管理内存的编程实践来回答问题,请这样做。 问题答案: 关于Java内存
本文向大家介绍简单了解python的内存管理机制,包括了简单了解python的内存管理机制的使用技巧和注意事项,需要的朋友参考一下 Python引入了一个机制:引用计数。 引用计数 python内部使用引用计数,来保持追踪内存中的对象,Python内部记录了对象有多少个引用,即引用计数,当对象被创建时就创建了一个引用计数,当对象不再需要时,这个对象的引用计数为0时,它被垃圾回收。 总结一下对象会在
问题内容: 多重处理是python中的强大工具,我想更深入地了解它。我想知道何时使用 常规的 锁和队列,何时使用多处理管理器在所有进程之间共享它们。 我提出了以下测试方案,其中包含四种不同的条件进行多处理: 使用池和 NO Manager 使用池和管理器 使用单个流程和 NO Manager 使用单个流程和一个经理 工作 所有条件都执行作业功能。包括一些通过锁固定的打印件。此外,该函数的输入只是放
3、修改中断向量的步骤 前面,分别介绍了读取和设置中断入口地址的方法。在编程的实际过程中,若要修改某个具体的中断处理程序时,一般需按下面几步来完成: 1)、读取指定中断的中断处理程序入口地址,并把它保存在存储单元内; 2)、把用户编写的程序段设置为指定中断的新处理程序; 3)、在用户程序结束之前(或不需要新的处理程序时),把步骤1保存的入口地址恢复成处理该中断的入口地址。 程序员按以上三步来编程,
8.3.7 内存管理的中断功能 在C/C++语言环境中,若事先不知道数据的容量,那么,可通过动态申请空间的方法来解决数据的存储问题,这种动态数据结构可用中断21H所提供的存储管理功能来实现。 中断21H在内存管理方面所提供的主要功能如下: 48H——分配内存块 49H——释放内存块 4AH——重定义内存块的大小 58H——读取/设置内存分配策略,其最基本的分配策略有三种:第一满足、最好满足和最后满