[转]常见的JavaScript内存泄露
什么是内存泄露
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。内存泄漏通常情况下只能由获得程序源代码的程序员才能分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,即使严格意义上来说这是不准确的。————wikipedia
⚠️注:下文中标注的CG是Chrome浏览器中Devtools的【Collect garbage】按钮缩写,表示回收垃圾操作。
意外的全局变量
JavaScript对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过delete
删除)。如果在浏览器中,全局对象就是window对象。
如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。
function foo(arg) {
bar = "this is a hidden global variable with a large of data";
}
等同于:
function foo(arg) {
window.bar = "this is an explicit global variable with a large of data";
}
另外,通过this创建意外的全局变量:
function foo() {
this.variable = "potential accidental global";
}
// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'
foo();
解决方法:
在JavaScript文件中添加'use strict'
,开启严格模式,可以有效地避免上述问题。
function foo(arg) {
"use strict" // 在foo函数作用域内开启严格模式
bar = "this is an explicit global variable with a large of data";// 报错:因为bar还没有被声明
}
如果需要在一个函数中使用全局变量,可以像如下代码所示,在window上明确声明:
function foo(arg) {
window.bar = "this is a explicit global variable with a large of data";
}
这样不仅可读性高,而且后期维护也方便
谈到全局变量,需要注意那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。全局变量也常用来做cache,一般cache都是为了性能优化才用到的,为了性能,最好对cache的大小做个上限限制。因为cache是不能被回收的,越高cache会导致越高的内存消耗。
console.log
console.log
:向web开发控制台打印一条消息,常用来在开发时调试分析。有时在开发时,需要打印一些对象信息,但发布时却忘记去掉console.log
语句,这可能造成内存泄露。
在传递给console.log
的对象是不能被垃圾回收 ♻️,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log
任何对象。
实例------>demos/log.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Leaker</title>
</head>
<body>
<input type="button" value="click">
<script>
!function () {
function Leaker() {
this.init();
};
Leaker.prototype = {
init: function () {
this.name = (Array(100000)).join('*');
console.log("Leaking an object %o: %o", (new Date()), this);// this对象不能被回收
},
destroy: function () {
// do something....
}
};
document.querySelector('input').addEventListener('click', function () {
new Leaker();
}, false);
}()
</script>
</body>
</html>
这里结合Chrome的Devtools–>Performance做一些分析,操作步骤如下:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 开启【Performance】项的记录
- 执行一次CG,创建基准参考线
- 连续单击【click】按钮三次,新建三个Leaker对象
- 执行一次CG
- 停止记录
可以看出【JS Heap】线最后没有降回到基准参考线的位置,显然存在没有被回收的内存。如果将代码修改为:
!function () {
function Leaker() {
this.init();
};
Leaker.prototype = {
init: function () {
this.name = (Array(100000)).join('*');
},
destroy: function () {
// do something....
}
};
document.querySelector('input').addEventListener('click', function () {
new Leaker();
}, false);
}()
去掉console.log("Leaking an object %o: %o", (new Date()), this);
语句。重复上述的操作步骤,分析结果如下:
从对比分析结果可知,console.log
打印的对象是不会被垃圾回收器回收的。因此最好不要在页面中console.log
任何大对象,这样可能会影响页面的整体性能,特别在生产环境中。除了console.log
外,另外还有console.dir
、console.error
、console.warn
等都存在类似的问题,这些细节需要特别的关注。
closures(闭包)
当一个函数A返回一个内联函数B,即使函数A执行完,函数B也能访问函数A作用域内的变量,这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。
function foo(message) {
function closure() {
console.log(message)
};
return closure;
}
// 使用
var bar = foo("hello closure!");
bar()// 返回 'hello closure!'
在函数foo内创建的函数closure对象是不能被回收掉的,因为它被全局变量bar引用,处于一直可访问状态。通过执行bar()
可以打印出hello closure!
。如果想释放掉可以将bar = null
即可。
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
实例------>demos/closures.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Closure</title>
</head>
<body>
<p>不断单击【click】按钮</p>
<button id="click_button">Click</button>
<script>
function f() {
var str = Array(10000).join('#');
var foo = {
name: 'foo'
}
function unused() {
var message = 'it is only a test message';
str = 'unused: ' + str;
}
function getData() {
return 'data';
}
return getData;
}
var list = [];
document.querySelector('#click_button').addEventListener('click', function () {
list.push(f());
}, false);
</script>
</body>
</html>
这里结合Chrome的Devtools->Memory工具进行分析,操作步骤如下:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 选中【Record allocation timeline】选项
- 执行一次CG
- 单击【start】按钮开始记录堆分析
- 连续单击【click】按钮十多次
- 停止记录堆分析
上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条,过滤出对应新分配的对象:
查看对象的详细信息:
从图可知,在返回的闭包作用链(Scopes)中携带有它所在函数的作用域,作用域中还包含一个str字段。而str字段并没有在返回getData()中使用过。为什么会存在在作用域中,按理应该被GC回收掉, why
原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因,便可以对症下药了。对代码做如下修改:
function f() {
var str = Array(10000).join('#');
var foo = {
name: 'foo'
}
function unused() {
var message = 'it is only a test message';
// str = 'unused: ' + str; //删除该条语句
}
function getData() {
return 'data';
}
return getData;
}
var list = [];
document.querySelector('#click_button').addEventListener('click', function () {
list.push(f());
}, false);
getData()和unused()内部函数共享f函数对应的变量对象,因为unused()内部函数访问了f作用域内str变量,所以str字段存在于f变量对象中。加上getData()内部函数被返回,被其他对象引用,形成了闭包,因此对应的f变量对象存在于闭包函数的作用域链中。这里只要将函数unused中str = 'unused: ' + str;
语句删除便可解决问题。
查看一下闭包信息:
DOM泄露
在JavaScript中,DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。如Chrome浏览器中DOM位于WebCore,而JavaScript/ECMAScript位于V8中。假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大影响。了解更多ℹ️
为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。
实例------>demos/dom.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Dom-Leakage</title>
</head>
<body>
<input type="button" value="add" class="add">
<input type="button" value="remove" class="remove" style="display:none;">
<div class="container">
<pre class="wrapper"></pre>
</div>
<script>
// 因为要多次用到pre.wrapper、div.container、input.remove、input.add节点,将其缓存到本地变量中
var wrapper = document.querySelector('.wrapper');
var container = document.querySelector('.container');
var removeBtn = document.querySelector('.remove');
var addBtn = document.querySelector('.add');
var counter = 0;
var once = true;
// 方法
var hide = function(target){
target.style.display = 'none';
}
var show = function(target){
target.style.display = 'inline-block';
}
// 回调函数
var removeCallback = function(){
removeBtn.removeEventListener('click', removeCallback, false);
addBtn.removeEventListener('click', addCallback, false);
hide(addBtn);
hide(removeBtn);
container.removeChild(wrapper);
}
var addCallback = function(){
wrapper.appendChild(document.createTextNode('\t' + ++counter + ':a new line text\n'));
// 显示删除操作按钮
if(once){
show(removeBtn);
once = false;
}
}
// 绑定事件
removeBtn.addEventListener('click', removeCallback, false);
addBtn.addEventListener('click', addCallback, false);
</script>
</body>
</html>
这里结合Chrome浏览器的Devtools–>Performance做一些分析,操作步骤如下:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 开启【Performance】项的记录
- 执行一次CG,创建基准参考线
- 连续单击【add】按钮6次,增加6个文本节点到pre元素中
- 单击【remove】按钮,删除刚增加6个文本节点和pre元元素
- 执行一次CG
- 停止记录堆分析
从分析结果图可知,虽然6次add操作增加6个Node,但是remove操作并没有让Nodes节点数下降,即remove操作失败。尽管还主动执行了一次CG操作,Nodes曲线也没有下降。因此可以断定内存泄露了!那问题来了,如何去查找问题的原因呢?这里可以通过Chrome浏览器的Devtools–>Memory进行诊断分析,执行如下操作步骤:
⚠️注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果
- 选中【Take heap snapshot】选项
- 连续单击【add】按钮6次,增加6个文本节点到pre元素中
- 单击【Take snapshot】按钮,执行一次堆快照
- 单击【remove】按钮,删除刚增加6个文本节点和pre元元素
- 单击【Take snapshot】按钮,执行一次堆快照
- 选中生成的第二个快照报告,并将视图由"Summary"切换到"Comparison"对比模式,在[class filter]过滤输入框中输入关键字:Detached
从分析结果图可知,导致整个pre元素和6个文本节点无法别回收的原因是:代码中存在全局变量wrapper
对pre元素的引用。知道了产生的问题原因,便可对症下药了。对代码做如下就修改:
// 因为要多次用到pre.wrapper、div.container、input.remove、input.add节点,将其缓存到本地变量中
var wrapper = document.querySelector('.wrapper');
var container = document.querySelector('.container');
var removeBtn = document.querySelector('.remove');
var addBtn = document.querySelector('.add');
var counter = 0;
var once = true;
// 方法
var hide = function(target){
target.style.display = 'none';
}
var show = function(target){
target.style.display = 'inline-block';
}
// 回调函数
var removeCallback = function(){
removeBtn.removeEventListener('click', removeCallback, false);
addBtn.removeEventListener('click', addCallback, false);
hide(addBtn);
hide(removeBtn);
container.removeChild(wrapper);
wrapper = null;//在执行删除操作时,将wrapper对pre节点的引用释放掉
}
var addCallback = function(){
wrapper.appendChild(document.createTextNode('\t' + ++counter + ':a new line text\n'));
// 显示删除操作按钮
if(once){
show(removeBtn);
once = false;
}
}
// 绑定事件
removeBtn.addEventListener('click', removeCallback, false);
addBtn.addEventListener('click', addCallback, false);
在执行删除操作时,将wrapper对pre节点的引用释放掉,即在删除逻辑中增加wrapper = null;
语句。再次在Devtools–>Performance中重复上述操作:
小试牛刀------>demos/dom_practice.html
再来看看网上的一个实例,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Practice</title>
</head>
<body>
<div id="refA"><ul><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#" id="refB"></a></li></ul></div>
<div></div>
<div></div>
<script>
var refA = document.getElementById('refA');
var refB = document.getElementById('refB');
document.body.removeChild(refA);
// #refA不能GC回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。
refA = null;
// 还存在变量refB对#refA的间接引用(refB引用了#refB,而#refB属于#refA)。将变量refB对#refB的引用释放,#refA就可以被GC回收。
refB = null;
</script>
</body>
</html>
整个过程如下图所演示:
有兴趣的同学可以使用Chrome的Devtools工具,验证一下分析结果,实践很重要~~~
timers
在JavaScript常用setInterval()
来实现一些动画效果。当然也可以使用链式setTimeout()
调用模式来实现:
setTimeout(function() {
// do something. . . .
setTimeout(arguments.callee, interval);
}, interval);
如果在不需要 什么是内存泄露 指由于疏忽或错误造成程序未能释放已经不再使用的内存.内存泄漏并非指内存在物理上的消失, 而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造 ... 总结一下常见的几种JavaScript内存泄露: 1.意外的全局变量 全局变量属于window对象,所以只会随着window销毁才会销毁. 2.console.log() conaole.log()函 ... 原文:4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them笔记:涂鸦码龙 译者注:本文并没有逐字逐句的翻译,而是把我认为重要 ... 本文原链接:https://cloud.tencent.com/developer/article/1340979 JavaScript 内存泄露的4种方式及如何避免 简介 什么是内存泄露? Java ... 内存泄露 在javascript中,我们非常少去关注内存的管理. 我们创建变量,使用变量,浏览器关注这些底层的细节都显得非常正常. 可是当应用程序变得越来越复杂而且ajax化之后,或者用户在一个页面停 ... 在过去一些的时候,Web开发人员并没有太多的去关注内存泄露问题.那时的页面间联系大都比较简单,并主要使用不同的连接地址在同一个站点中导航,这样的设计方式是非常有利于浏览器释放资源的.即使Web页面运行 ... 原文地址:http://javascript.info/tutorial/memory-leaks#tools 我们在进行JavaScript开发时,很少会考虑内存的管理.JavaScript中变量的 ... 一.前言 一直有打算总结一下JS内存泄露的方面的知识的想法,但是总是懒得提笔. 富兰克林曾经说过:懒惰,像生鏽一样,比操劳更能消耗身体,经常用的钥匙总是亮闪闪的.安利一下,先起个头. 二.内存声明周期 ... 高效的JavaScript Web应用必须流畅,快速.与用户交互的任何应用程序,都需要考虑如何确保内存有效使用,因为如果消耗过多,页面就会崩溃,迫使用户重新加载.而你只能躲在角落哭泣. 自动垃圾收集是 ... 在javaScript系列 [01]-javaScript函数基础这篇文章中我已经简单介绍了JavaScript语言在函数使用中this的指向问题,虽然篇幅不长,但其实最重要的部分已经讲清楚了,这篇文 ... public Boolean exist(Container container){ List<SensorAtom> newSensorList = container.getSenso ... Netty buffer缓冲区ByteBuf byte 作为网络传输的基本单位,因此数据在网络中进行传输时需要将数据转换成byte进行传输.netty提供了专门的缓冲区byte生成api ByteBu ... PLC是嵌入式系统吗? 一.PLC即Programmable Logic Controller,可编程逻辑控制器 单片机又名MCU即microprocessor control unit,微处理机控制 ... Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ... 前言: 考虑自己网站的图片展示,而且要支持移动端和PC端.自己写的代码也不尽如意,要写好的话也需要时间,于是就想到了使用相关插件. 准备: PhotoSwipe 官网地址:http://photosw ... Darwin Streaming Server中mp4文件点播失败,通过抓包发现服务器返回”415 Unsupported Media Type“错误,如下: RTSP/ Unsupported Me ... 微软在06月27日发布了Visual Studio 2015 Update 3 .在MSDN中微软也提供下载,而且MSDN的Visual Studio 2015 Update 3与官方免费下载的文件是 ... 信号及信号来源 什么是信号 信号是UNIX和Linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动.通常信号是由一个错误产生的.但它们还可以作为进程间通信或修改行为的一种方 ... linux ls命令的用法大全 学习linux这么久了,最常用的命令莫属 ls命令了,今天就总结下ls命令的用法与经验技巧. ls命令按文件大小查看文件 a.降序:ls -lsh moudae ...setInterval()
时,没有通过clearInterval()
方法移除,那么setInterval()
会不停地调用函数,直到调用clearInterval()
或窗口关闭。如果链式setTimeout()
调用模式没有给出终止逻辑,也会一直运行下去。因此再不需要重复定时器时,确保对定时器进行清除,避免占用系统资源。另外,在使用setInterval()
和setTimeout()
来实现动画时,无法确保定时器按照指定的时间间隔来执行动画。为了能在JavaScript中创建出平滑流畅的动画,浏览器为JavaScript动画添加了一个新API-requestAnimationFrame()。关于setInterval、setTimeout与requestAnimationFrame实现动画上的区别➹猛击
[转]常见的JavaScript内存泄露的更多相关文章
随机推荐