像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,一个是oao2 = "yo"; // 最初的对象现在已经是零引用了// 他可以被垃圾回收了// 然而它的属性a的对象还在被oa引用,所以还不能回收oa = null; // a属性的那个对象现在也是零引用了// 它可以被垃圾回收了
复制代码

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

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

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

function f(){var o = {};var o2 = {};o.a = o2; // o 引用 o2o2.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 引用 o2o2.a = o; // o2 引用 oreturn "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 进程的内存占用信息。

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

  • rss(resident set size):所有内存占用,包括指令区和堆栈。
  • heapTotal:"堆"占用的内存,包括用到的和没用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

常见的内存泄露案例

意外的全局变量

function foo() {bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1this.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 元素的引用。

div中定义局部变量_中高级前端必须要了解的--JS中的内存管理相关推荐

  1. div中定义局部变量_说说 Go 中的变量(附粗制滥造面试题)

    和其他语言没有区别,Go 中的数据也是两种表示方式,常量和变量,本文先说说变量吧. 为了增加文章的趣味性(多掉些头发),搜集了一些常见的面试题.部分是自己瞎编的,顺便为自己明年可能到来的面试做些准备. ...

  2. mysql中begin end中变量定义_DECLARE只能用于BEGIN…END的开头部分定义局部变量,其作用范围只能在该BEGIN…END中。_学小易找答案...

    [单选题]表达式select ((6%(7-5))+8)*9-2+(5%2) 的运算结果是______. [判断题]所有变量名@var1.@var2等必须以1个"@"开头,可以由当 ...

  3. (中篇)中高级前端大厂面试秘籍,寒冬中为您保驾护航,直通大厂

    感恩!~~没想到上篇文章能这么受大家的喜欢,激动不已.?.但是却也是诚惶诚恐,这也意味着责任.下篇许多知识点都需要比较深入的研究和理解,博主也是水平有限,担心自己无法承担大家的期待.不过终究还是需要摆 ...

  4. python 类定义 垃圾_什么是python对象摧毁?python中的对象摧毁(垃圾回收)机制是什么?...

    在这篇文章之中我们来了解一下python对象摧毁(垃圾回收),对于刚刚接触到python这一编程语言的朋友来说,对于python对象摧毁(垃圾回收)的了解应该比较少,并且不清楚关于python垃圾回收 ...

  5. java中抽象接口_一篇文章让你彻底理解java中抽象类和接口

    相信大家都有这种感觉:抽象类与接口这两者有太多相似的地方,又有太多不同的地方.往往这二者可以让初学者摸不着头脑,无论是在实际编程的时候,还是在面试的时候,抽象类与接口都显得格外重要!希望看完这篇博客文 ...

  6. Java中合法的关键词_优秀程序员必须掌握的java中50个关键字

    关键字和保留字的区别 正确识别java语言的关键字(keyword)和保留字(reserved word)是十分重要的.Java的关键字对java的编译器有特殊的意义,他们用来表示一种数据类型,或者表 ...

  7. kafka查看topic中的数据_实战!Kafka Manager能统计出Topic中的记录条数吗?

    问题描述 今天现场实施同事说Kafka Manager上显示有3500w条记录,但使用我们的平台落地后,一统计发现只有2200w条记录,这是不是说明我们的平台存在丢数据的可能. 经了解,对接方是通过如 ...

  8. 在java中添加源_关于Java:如何在Android Studio中添加链接的源文件夹?

    在Eclipse中,我可以将源文件夹作为"链接的源文件夹"添加到我的android项目中. 如何在Android Studio中实现同一目的? 或者是否可以添加外部文件夹以构建gr ...

  9. carsim中质心加速度_振动CAE分析在空调压缩机支架设计中的应用

    [摘要]本文运用有限元分析方法分析空调压缩机系统模态,并通过分析引起振动的激励源,找出压缩机支架和安装螺栓断裂的根本原因,并根据分析提出了解决措施.关键词:空调压缩机支架模态激励共振一.引言发动机轮系 ...

最新文章

  1. android xml opacity,Android Drawable详解
  2. Python-OpenCV 笔记7 -- 绘图(Draw)
  3. python多继承顺序_Python多继承以及MRO顺序的使用
  4. LeetCode 374. Guess Number Higher or Lower
  5. 表哥首发送书100本,感谢老铁们支持!
  6. matlab三次样条拟合,【MATLAB编程】三次样条
  7. 转:如何在Ubuntu系统下安装使用LaTeX
  8. python运行快捷键是什么_Python快捷键
  9. c语言列宽作用,c语言|格式化输入输出详解
  10. 【考研数学】琴生不等式
  11. linux 谷歌日语输入法下载软件,Linux Mint---fcitx中文,日语输入法
  12. U盘安装纯净版的win7系统
  13. 汉字转拼音之Jpinyin 简单使用
  14. 在WinServer2008下安装SQLServer2014
  15. C#编写画直线,简单画线,鼠标交互画线,画一条线
  16. 利用CyclicBarrier实现赛马游戏
  17. Matlab GUI编程技巧(十五):scroll滚动到组件内的位置及ScrollBar动画演示
  18. OV7725引脚配置
  19. win7开机卡在桌面无图标,任务管理器打不开的一种解决办法
  20. ***入门教程1-57集!

热门文章

  1. 5G应用前景广泛 不止是下电影更快,还能做这些事……
  2. Luogu 4069 [SDOI2016]游戏
  3. 用户故事与敏捷方法笔记 --- 用户故事
  4. RANDOM模块:PYTHON获取随机数
  5. thinkphp 插件
  6. Ubuntu 左边栏和顶栏都不见了,ctrl+alt+t 也调用不出terminal
  7. 泛型ListT排序(利用反射)
  8. 网上Silverlight项目收集
  9. [转载] Python 3.x | 史上最详解的 导入(import)
  10. [转载] python画柱状图-Python绘制精美图表之双柱形图