原文:http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html

本周我在Meter的同事追踪到了一个奇怪的Javascript内存泄漏。我找遍了互联网,尝试了各种关键字:javascript closure memory leak,无果。所以,这可能是一个未知的问题。(你们所找到的都是讲老版本的IE的垃圾回收算法的问题,但是我碰到的这个问题甚至影响到我当前装的最新Chrome浏览器。)

Update:Vyacheslav Egorov向我指出他曾经写过的一篇同样主题的文章,这篇文章有更详细的样例,更好的配图,加上一些V8内部机制的知识,很棒的一篇文章。甚至,还有一篇论文专门来讲这个问题的最佳实现方式,但是这篇文章并不是用Javascipt来说明的。

考虑如下代码:

var run = function () {
var str = new Array(1000000).join('*');
var doSomethingWithStr = function () {
if (str === 'something')
console.log("str was something");
};
doSomethingWithStr();
};
setInterval(run, 1000);

每一秒钟执行一次run函数,它会申请一个巨大的字符串,创建一个闭包来使用它,调用闭包,然后返回。闭包返回之后,它会被垃圾回收,因为没有什么引用str对象了,所以str也被回收。 但是如果我们在run里面持续调用闭包呢?

var run = function () {
var str = new Array(1000000).join('*');
var logIt = function () {
console.log('interval');
};
setInterval(logIt, 100);
};
setInterval(run, 1000);

每一秒钟run申请一个巨大的字符串,并且在内部调用logIt 10次,logIt持续运行,而str在它的词法作用域内,所以这可能构成内存泄漏。 幸运的是,JavaScript的引擎(至少在最新的Chrome)足够聪明,它能检测到logIt并没有使用到str,所以str不会被放到logIt的词法作用域环境,所以呢每次run结束后,str被GC是没有问题的。 太棒了,JavaScript能帮助我们防止内存泄漏,对吗?让我们来点更复杂的,把前两个例子合并起来。

var run = function () {
var str = new Array(1000000).join('*');
var doSomethingWithStr = function () {
if (str === 'something')
console.log("str was something");
};
doSomethingWithStr();
var logIt = function () {
console.log('interval');
}
setInterval(logIt, 100);
};
setInterval(run, 1000);

打开Chrome开发者工具,切换到Memory View视图,然后点record。

看起来每秒钟都会增加1M的内存使用,甚至点击垃圾回收按钮来强直GC也不起作用,所以看起来str泄漏了。 不过这个看起来不是和之前的一样的情形吗,str只被run和doSomethingWithStr引用了。一旦run运行结束,doSomeThingWithStr会被回收。run里唯一一个引起泄漏的便是第二个闭包:logIt,并且logIt根本没指向str。 所以,尽管没有任何代码指向str,它也没有被GC,为什么呢?

好吧,闭包的典型实现是每一个函数对象有一个类似字典的对象来代表它的词法环境。如果定义在run里的函数确实使用了str,即使str被一次又一次的赋值,它也能保证两者引用的是相同的对象。如果变量没有被任何闭包使用的话,V8引擎足够聪明来让变量在函数的词法环境之外,所以第一个示例没有发生内存泄漏。 但是一旦一个变量被任一个闭包使用了,它会在所有的闭包词法环境结束之后才被释放,这会导致内存泄漏。

到这里,你能想到一个更智能的词法环境的实现来避免这个问题。每一个闭包用一个字典记录它真正使用的变量,字典里的变量应当是一个可变单元,能被多个闭包共享词法环境。根据我阅读ECMA V5文档,这个方法是的合法的。ECMA文档指出词法环境是纯粹的规范机制,而不需要与EcmaScript的实现保持一致。

我们在Meter碰到的问题看起来是这样:我们只是希望用一个新对象取代一个对象,并且原来的对象被释放。

var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
// 定义一个引用originalThing的闭包但是没有真正调用的
// 但是因为闭包的存在,originalThing会在词法环境中
//所有定义在replaceThing的闭包都能访问。
//你把这个函数去掉,就没有泄漏了。
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
// originalThing理论上会被这个someMethod使用
//虽然很明显这个函数没有使用它。
//但是因为originalThing是词法作用域的一部分,someMethod会保持对originalThing的引用。
//尽管我们每次调用都会把theThing覆盖但是原先的值永远不会被清除。
someMethod: function () {}
};
// 如果你在这里加上 `originalThing = null` 就不会泄漏了
};
setInterval(replaceThing, 1000);

很显然someMethod是个闭包,repalceThing执行结束后,因为theThing还引用着someMethod,originalThing就不会被释放。

一旦了解了这些,解决这个问题就很简单了。总结一下:如果你有一个大的对象被一些闭包使用,但是不是每一个闭包使用。只要保证使用完后本地变量不再指向它。不幸的是,这些Bug很难察觉。如果Javascript能让你不用考虑就好了。

一个意想不到的Javascript内存泄漏的更多相关文章

  1. Javascript内存泄漏

    Javascript内存泄漏 原文:http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html 本周 ...

  2. 关于JavaScript内存泄漏的质疑

    近几天看了些关于JavaScript内存管理的文章,相对于Java JVM的内存管理,显得简单些. 在学习的过程中,发现有不少网友谈到了循环引用,说循环引用会造成内存泄漏,垃圾回收器无法回收. 实际上 ...

  3. 介绍两个非常好用的Javascript内存泄漏检测工具

    内存泄漏对开发者来说一般很难检测因为它们是由一些大量代码中的意外的错误引起的,但它在系统内存不足前并不影响程序的功能.这就是为什么会有人在很长时间的测试期中收集应用程序性能指标来测试性能. 最简单的检 ...

  4. JavaScript学习总结(二十三)——JavaScript 内存泄漏教程

    参考教程:http://www.ruanyifeng.com/blog/2017/04/memory-leak.html 一.什么是内存泄漏? 程序的运行需要内存.只要程序提出要求,操作系统或者运行时 ...

  5. JavaScript内存泄漏知多少?

    垃圾回收解放了我们,它让我们可将精力集中在应用程序逻辑(而不是内存管理)上.但是,垃圾收集并不神奇.了解它的工作原理,以及如何使它保留本应在很久以前释放的内存,就可以实现更快更可靠的应用程序.在本文中 ...

  6. JavaScript 内存泄漏教程

    一.什么是内存泄漏? 程序的运行需要内存.只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存. 对于持续运行的服务进程(daemon),必须及时释放不再用到的内存.否则,内存占用越来 ...

  7. c++:一个辅助类让内存泄漏现原形!

    前言 对于c++而言,如何查找内存泄漏是程序员亘古不变的话题:解决之道可谓花样繁多.因为最近要用到QT写程序,摆在我面前的第一个重要问题是内存防泄漏.如果能找到一个简单而行之有效的方法,对后续开发大有 ...

  8. 了解 JavaScript 应用程序中的内存泄漏

    简介 当处理 JavaScript 这样的脚本语言时,很容易忘记每个对象.类.字符串.数字和方法都需要分配和保留内存.语言和运行时的垃圾回收器隐藏了内存分配和释放的具体细节. 许多功能无需考虑内存管理 ...

  9. JavaScript中的内存泄漏以及如何处理

    随着现在的编程语言功能越来越成熟.复杂,内存管理也容易被大家忽略.本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题. 概 ...

随机推荐

  1. jQuery整理您的笔记----jQuery开始

    Jquery它是一种高速.简明的JavaScript相框,jQuery设计目标:Write Less,Do More(写更少的代码,做很多其他的事情). 一.Jquery框架优势: 1.轻量级 jQu ...

  2. MVC 6 写法

    MVC 6 一些不晓得的写法 今天在看 Scott Guthrie 的一篇博文<Introducing ASP.NET 5>,在 MVC 6 中,发现有些之前不晓得的写法,这边简单记录下, ...

  3. angularJS看MVVM

    从angularJS看MVVM   javascript厚积薄发走势异常迅猛,导致现在各种MV*框架百家争雄,MVVM从MVC演变而来,为javascript注入了全新的活力.我工作的业务不会涉及到a ...

  4. 在asp.net webservice中如何使用session

    原文:在asp.net webservice中如何使用session 原文:刘武|在asp.net webservice中如何使用session 在使用asp.net编写webservice时,默认情 ...

  5. EA强大的绘图工具---设计数据库表格

    关于EA这个优秀的软件是从师哥哪里听来的,自己瞎点了点,感觉也没什么.近期和和智福加上一个师哥合作敲机房收费系统时,想到之前听人说EA非常强大,便随便找了找关于EA使用的帮助手冊.果然惊喜-- 如题, ...

  6. java 转成字符串 json 数组和迭代

    当你需要转成一串一串的json 排列 .当内容和遍历它们. 首页进口 net.sf.json.JSONArray和net.sf.json.JSONObject 两个jar 包 String str = ...

  7. 4GB内存原32位系统(x86)取舍问题,显卡共享内存Win8.1完全不用担心

    情景:集成显卡 配置: 4G显示3.25GB 此时系统自动将用不到的系统完全共享给显卡(768MB而不是256): 看显卡适配器信息,完全共享给了显卡 解说:上图总可用图形内存 = 图2中备用 + 硬 ...

  8. 2.1 LINQ的查询表达式

    在进行LINQ查询的编写之前,首先需要了解查询表达式.查询表达式是LINQ查询的基础,也是最常用的编写LINQ查询的方法. 查询表达式由查询关键字和对应的操作数组成的表达式整体.其中,查询关键字是常用 ...

  9. [译]Java 设计模式之命令

    (文章翻译自Java Design Pattern: Command) 命令设计模式在进行执行和记录的时候需要一个操作及其参数和封装在一个对象里面.在下面的例子中,命令是一个操作,它的参数是一个Com ...

  10. HubbleDotNet全文搜索数据库组件(二)

    [摘要]本文介绍如何使用HubbleDotNet实现基本的全文搜索,包括建立搜索数据库.数据表.建立索引,压缩索引和搜索示例等内容. 上文介绍了HubbleDotNet的安装,接下来介绍如何使用Hub ...