JavaScript中的内存泄漏以及如何处理
title: JavaScript中的内存泄漏以及如何处理
date: 2017-11-21 21:27:04
tags:
随着现代编程语言功能越来越成熟、复杂,内存管理也容易被大家忽略。本文将讨论Javascript中的内存泄露以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄露带来的问题。
概述
当创建对象和字符串等时,JavaScript就会分配内存,并在不使用时自动释放内存,这种机制被称为垃圾收集。这种释放资源看似是“自动”的,但本质是混淆的,这也给JavaScript的开发人员产生了可以不关心内存管理的错误印象。其实这是一个大错误。
什么是内存泄露
内存泄露是应用程序使用过的内存片段,在不再需要时,不能返回到操作系统或可用内存池中的情况。
编程语言有各自不同的内存管理方式。但是是否使用某一段内存,实际上是一个不可判定的问题。换句话说,只有开发人员明确的知道是否需要将一块内存返回给操作系统。
四种常见的JavaScript内存泄露
1.全局变量
JavaScript以一种有趣的方式来处理未声明的变量:当引用未声明的变量时,会在全局对象中创建一个新变量。在浏览器中,全局对象将是window,这意味着
function foo(arg){
bar = "some text";
}
相当于:
function foo(arg){
window.bar = "some text";
}
bar只是foo函数中引用一个变量。如果你不适用var声明,将会创建u多余的全局变量。在上述情况下,不会造成很大的问题。但是,若是下面的这种情况。你可能不小心创建一个全局变量this:
function foo(){
this.val1 = "potential accidental global";
}
// Foo called on its own, this points to the global object (window) rather than being undefined.
foo();
你可以通过在JavaScript文件的开始处添加‘use strict’;来避免这种错误,这种方式将开启严格的解析JavaScript模式,从而防止意外创建全局变量。
意外的全局变量当然是一个问题。更多的时候,你的代码会受到显示的全局变量的影响,而这些全局变量在垃圾收集器中是无法收集。需要特别注意用于临时存储和处理大量信息的全局变量。如果必须使用全局变量来存储数据,那么确保将其分配为空值,或者在完成后重新分配。
2.被遗忘的定时器或回调
下面列举setInterval的例子,这也是经常在JavaScript中使用。
对于提供监视的库和其他接收回调的工具,通常在确保所有回调的引用在其实例无法访问时,会变成无法访问的状态。但是下面的代码却是一个例外:
var serverData = loadData();
setInterval(function(){
var renderer = document.getElementById('render');
if(renderer){
renderer.innerHTML = JSON.stringify(serverData);
}
},5000); // This will be executed every ~5 seconds.
上面的代码片段显示了使用引用节点或不再需要的数据的定时器的结果。
该renderer对象可能会在某些时候被替换或删除,这会使interval处理程序封装的块变得冗余。如果发生这种情况,那么处理程序及其依赖项都不会被收集,因为interval需要先停止。这一切都归结为存储和处理负载数据的serverData不会被收集的原因。
当使用监视器时,你需要确保做了一个明确的调用来删除它们。
幸运的是,大多数现代浏览器都会为你做这件事:即使你忘记删除监听器,当监测对象变得无法访问,它们就会自动收集监测处理器。这是过去的一些浏览器无法处理的情况(例如旧的IE6)。
看下面的例子:
var element = document.getElementById('launch-button');
var counter = 0;
function onclick(event){
counter++;
element.innerHTML = 'text '+ counter;
}
element.addEventListener('click',onClick);
// Do stuff
element.removeEventListener('click',onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope, both element and onClick will be collected event in old browsers that don't handle cycles well.
由于现代浏览器支持垃圾回收机制,所以当某个节点变得不能访问时,你不再需要调用removeEventListener,因为垃圾回收机制会恰当的处理这些节点。
如果你正在使用jQueryAPI(其他库和框架也支持这一点),那么也可以在节点不用之前删除监听器。即使应用程序在较旧的浏览器版本下运行,库也会确保没有内存泄露。
3.闭包
JavaScript开发的是一个关键方面是闭包。闭包是一个内部函数,可以访问外部(封闭函数)函数的变量。由于JavaScript运行时的实现细节,可能存在一下形式内存泄露:
var theThing = null;
var replaceThing = function(){
var originalThing = theThis;
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会获取由一个大数组和一个新的闭包(someMthod)组成的新对象。然而,originalThing会被unused变量所持有的闭包所引用(这是theThing从以前的调用变量replaceThing)。需要记住的是,一旦在同一父作用域中为闭包创建了闭包的作用域,作用域就被共享了。
在这种情况下,闭包创建的范围会将someMethod共享给unused。然而,unused有一个originalThing引用。即使unused从未使用过,someMethod也可以通过theThing在整个范围之外使用replaceThing。而且someMethod通过unused共享了闭包范围,unused必须引用originalThing以便使其它保持活跃(两封闭之间的整个共享范围)。这就阻止了它被收集。
所有这些都可能导致相当大的内存泄露。当上面的代码片段一遍又一遍地运行时,你会看到内存使用率的不断上升。当垃圾收集器运行时,其内存大小不会缩小,这种情况会创建一个闭包的链表,并且每个闭包范围都带有对大数组的间接引用。
4.超出DOM引用
在某些情况下,开发人员会在数据结构中存储DOM节点,例如你想快速更新表格中的几行内的情况。如果在字典或数组中存储对每个DOM行的引用,则会有两个对同一个DOM元素的引用:一个在DOM树中,另一个在字典中。如果你不再需要这些行,则需要使两个引用都无法访问。
var elements = {
button:document.getElementById('button'),
image:document.getElementById('image'),
};
function doStuff(){
elements.image.src = "http://example.com/image_name.png";
}
function removeImage(){
// The image is a direct child of the body element.
document.body.removeChild(document.getElementById('image'));
// At this point, we still have a reference to #button in the global elements object, In other words, the button element is still in memory and cannot be collected by the GC.
}
在涉及DOM树内的内部节点或叶节点时,还有一个额外的因素需要考虑。如果你在代码中保留对表格单元格(标签)的引用,并决定从DOM中删除该表格,还需要保留对该特定单元格的引用,则可能会出现严重的内存泄露,你可能会认为垃圾收集器会释放除了那个单元之外的所有东西,但情况并非如此。由于单元格是表格的一个子节点,并且子节点保留着对父节点的引用,所以对表格单元格的这种引用,会将整个表格保存在内存中。
总结
以上内容是对JavaScript常见的四种内存泄露分析。希望对JavaScript编程人员有用。