关于Javascript的内存泄漏问题的整理稿
写了好长时间javascript小功能模块,从来没有关注过内存泄漏问题。记得以前写C++程序的时候,内存泄漏是个严重的问题,我想是时候关注一下了。网上找了篇文章,Mark一下。原文地址:http://www.blogjava.net/tim-wu/archive/2006/05/29/48729.html
常规循环引用内存泄漏和Closure内存泄漏
要了解javascript的内存泄漏问题,首先要了解的就是javascript的GC原理。
我记得原来在犀牛书《JavaScript: The Definitive Guide》中看到过,IE使用的GC算法是计数器,因此只碰到循环 引用就会造成memory leakage。后来一直觉得和观察到的现象很不一致,直到看到Eric的文章,才明白犀牛书的说法没有说得很明确,估计该书成文后IE升级过算法吧。 在IE 6中,对于javascript object内部,jscript使用的是mark-and-sweep算法,而对于javascript object与外部object(包括native object和vbscript object等等)的引用时,IE 6使用的才是计数器的算法。
Eric Lippert在http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx一文中提到IE 6中JScript的GC算法使用的是nongeneration mark-and-sweep。对于javascript对算法的实现缺陷,文章如是说:
"The benefits of this approach are numerous, but the principle benefit is that circular references are not leaked unless the circular reference involves an object not owned by JScript. "
也就是说,IE 6对于纯粹的Script Objects间的Circular References是可以正确处理的,可惜它处理不了的是JScript与Native Object(例如Dom、ActiveX Object)之间的Circular References。
所以,当我们出现Native对象(例如Dom、ActiveX Object)与Javascript对象间的循环引用时,内存泄露的问题就出现了。当然,这个bug在IE 7中已经被修复了[http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html]。
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp 中有个示意图和简单的例子体现了这个问题:
- < html >
- < head >
- < script language = " JScript " >
- var myGlobalObject;
- function SetupLeak() // 产生循环引用,因此会造成内存泄露
- {
- // First set up the script scope to element reference
- myGlobalObject =
- document.getElementById( " LeakedDiv " );
- // Next set up the element to script scope reference
- document.getElementById( " LeakedDiv " ).expandoProperty =
- myGlobalObject;
- }
- function BreakLeak() // 解开循环引用,解决内存泄露问题
- {
- document.getElementById( " LeakedDiv " ).expandoProperty =
- null ;
- }
- </ script >
- </ head >
- < body onload = " SetupLeak() " onunload = " BreakLeak() " >
- < div id = " LeakedDiv " ></ div >
- </ body >
- </ html >
上面这个例子,看似很简单就能够解决内存泄露的问题。可惜的是,当我们的代码中的结构复杂了以后,造成循环引用的原因开始变得多样,我们就没法那么容易观察到了,这时候,我们必须对代码进行仔细的检查。
尤其是当碰到Closure,当我们往Native对象(例如Dom对象、ActiveX Object)上绑定事件响应代码时,一个不小心,我们就会制造出Closure Memory Leak。其关键原因,其实和前者是一样的,也是一个跨javascript object和native object的循环引用。只是代码更为隐蔽,这个隐蔽性,是由于javascript的语言特性造成的。但在使用类似内嵌函数的时候,内嵌的函数有拥有一 个reference指向外部函数的scope,包括外部函数的参数,因此也就很容易造成一个很隐蔽的循环引用,例如:
DOM_Node.onevent ->function_object.[ [ scope ] ] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。
[http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp]有个例子极深刻地显示了该隐蔽性:
- < html >
- < head >
- < script language = " JScript " >
- function AttachEvents(element)
- {
- // This structure causes element to ref ClickEventHandler //element有个引用指向函数ClickEventHandler()
- element.attachEvent( " onclick " , ClickEventHandler);
- function ClickEventHandler()
- {
- // This closure refs element //该函数有个引用指向AttachEvents(element)调用Scope,也就是执行了参数element。
- }
- }
- function SetupLeak()
- {
- // The leak happens all at once
- AttachEvents(document.getElementById( " LeakedDiv " ));
- }
- </ script >
- </ head >
- < body onload = " SetupLeak() " onunload = " BreakLeak() " >
- < div id = " LeakedDiv " ></ div >
- </ body >
- </ html >
还有这个例子在IE 6中同样原因会引起泄露
- function leakmaybe() {
- var elm = document.createElement( " DIV " );
- elm.onclick = function () {
- return 2 + 2 ;
- }
- }
- for ( var i = 0 ; i 10000 ; i ++ ) {
- leakmaybe();
- }
btw:
关于Closure的知识,大家可以看看http://jibbering.com/faq/faq_notes/closures.html这篇文章,习惯中文也可以看看zkjbeyond的blog,他对Closure这篇文章进行了简要的翻译:http://www.blogjava.net/zkjbeyond/archive/2006/05/19/47025.html。 之所以会有这一系列的问题,关键就在于javascript是种函数式脚本解析语言,因此javascript中“函数中的变量的作用域是定义作用域,而 不是动态作用域”,这点在犀牛书《JavaScript: The Definitive Guide》中的“Funtion”一章中有所讨论。
http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555中也对这个问题举了很详细的例子。
一些 简单的解决方案
目前大多数ajax前端的javascript framework都利用对事件的管理,解决了该问题。
如果你需要自己解决这个问题,可以参考以下的一些方法:
http://outofhanwell.com/ieleak/index.php?title=Main_Page:有个不错的检测工具
http://youngpup.net/2005/0221010713 中提到:可以利用递归Dom树,解除event绑定,从而解除循环引用:
- if (window.attachEvent) {
- var clearElementProps = [
- 'data',
- 'onmouseover',
- 'onmouseout',
- 'onmousedown',
- 'onmouseup',
- 'ondblclick',
- 'onclick',
- 'onselectstart',
- 'oncontextmenu'
- ];
- window.attachEvent("onunload", function() {
- var el;
- for(var d = document.all.length;d--;){
- el = document.all[d];
- for(var c = clearElementProps.length;c--;){
- el[clearElementProps[c]] = null;
- }
- }
- });
- }
而http://novemberborn.net/javascript/event-cache一文中则通过增加EventCache,从而给出一个相对结构化的解决方案
- /* EventCache Version 1.0
- Copyright 2005 Mark Wubben
- Provides a way for automagically removing events from nodes and thus preventing memory leakage.
- See <http://novemberborn.net/javascript/event-cache> for more information.
- This software is licensed under the CC-GNU LGPL <http://creativecommons.org/licenses/LGPL/2.1/>
- */
- /* Implement array.push for browsers which don't support it natively.
- Please remove this if it's already in other code */
- if (Array.prototype.push == null ){
- Array.prototype.push = function (){
- for ( var i = 0 ; i < arguments.length; i ++ ){
- this [ this .length] = arguments[i];
- };
- return this .length;
- };
- };
- /* Event Cache uses an anonymous function to create a hidden scope chain.
- This is to prevent scoping issues. */
- var EventCache = function (){
- var listEvents = [];
- return {
- listEvents : listEvents,
- add : function (node, sEventName, fHandler, bCapture){
- listEvents.push(arguments);
- },
- flush : function (){
- var i, item;
- for (i = listEvents.length - 1 ; i >= 0 ; ii = i - 1 ){
- item = listEvents[i];
- if (item[ 0 ].removeEventListener){
- item[ 0 ].removeEventListener(item[ 1 ], item[ 2 ], item[ 3 ]);
- };
- /* From this point on we need the event names to be prefixed with 'on" */
- if (item[ 1 ].substring( 0 , 2 ) != " on " ){
- item[ 1 ] = " on " + item[ 1 ];
- };
- if (item[ 0 ].detachEvent){
- item[ 0 ].detachEvent(item[ 1 ], item[ 2 ]);
- };
- item[ 0 ][item[ 1 ]] = null ;
- };
- }
- };
- }();
使用方法也很简单:
- <script type="text/javascript">
- function addEvent(oEventTarget, sEventType, fDest){
- if(oEventTarget.attachEvent){
- oEventTarget.attachEvent("on" + sEventType, fDest);
- } elseif(oEventTarget.addEventListener){
- oEventTarget.addEventListener(sEventType, fDest, true);
- } elseif(typeof oEventTarget[sEventType] == "function"){
- var fOld = oEventTarget[sEventType];
- oEventTarget[sEventType] = function(e){ fOld(e); fDest(e); };
- } else {
- oEventTarget[sEventType] = fDest;
- };
- /* Implementing EventCache for all event systems */
- EventCache.add(oEventTarget, sEventType, fDest, true);
- };
- function createLeak(){
- var body = document.body;
- function someHandler(){
- return body;
- };
- addEvent(body, "click", someHandler);
- };
- window.onload = function(){
- var i = 500;
- while(i > 0){
- createLeak();
- ii = i - 1;
- }
- };
- window.onunload = EventCache.flush;
- </script>
http://talideon.com/weblog/2005/03/js-memory-leaks.cfm 一文中的方法类似:
- /*
- * EventManager.js
- * by Keith Gaughan
- *
- * This allows event handlers to be registered unobtrusively, and cleans
- * them up on unload to prevent memory leaks.
- *
- * Copyright (c) Keith Gaughan, 2005.
- *
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Common Public License v1.0
- * (CPL) which accompanies this distribution, and is available at
- * http://www.opensource.org/licenses/cpl.php
- *
- * This software is covered by a modified version of the Common Public License
- * (CPL), where Keith Gaughan is the Agreement Steward, and the licensing
- * agreement is covered by the laws of the Republic of Ireland.
- */
- // For implementations that don't include the push() methods for arrays.
- if ( ! Array.prototype.push) {
- Array.prototype.push = function (elem) {
- this [ this .length] = elem;
- }
- }
- var EventManager = {
- _registry: null ,
- Initialise: function () {
- if ( this ._registry == null ) {
- this ._registry = [];
- // Register the cleanup handler on page unload.
- EventManager.Add(window, " unload " , this .CleanUp);
- }
- },
- /* *
- * Registers an event and handler with the manager.
- *
- * @param obj Object handler will be attached to.
- * @param type Name of event handler responds to.
- * @param fn Handler function.
- * @param useCapture Use event capture. False by default.
- * If you don't understand this, ignore it.
- *
- * @return True if handler registered, else false.
- */
- Add: function (obj, type, fn, useCapture) {
- this .Initialise();
- // If a string was passed in, it's an id.
- if ( typeof obj == " string " ) {
- obj = document.getElementById(obj);
- }
- if (obj == null || fn == null ) {
- return false ;
- }
- // Mozilla/W3C listeners?
- if (obj.addEventListener) {
- obj.addEventListener(type, fn, useCapture);
- this ._registry.push({obj: obj, type: type, fn: fn, useCapture: useCapture});
- return true ;
- }
- // IE-style listeners?
- if (obj.attachEvent && obj.attachEvent( " on " + type, fn)) {
- this ._registry.push({obj: obj, type: type, fn: fn, useCapture: false });
- return true ;
- }
- return false ;
- },
- /* *
- * Cleans up all the registered event handlers.
- */
- CleanUp: function () {
- for ( var i = 0 ; i < EventManager._registry.length; i ++ ) {
- with (EventManager._registry[i]) {
- // Mozilla/W3C listeners?
- if (obj.removeEventListener) {
- obj.removeEventListener(type, fn, useCapture);
- }
- // IE-style listeners?
- else if (obj.detachEvent) {
- obj.detachEvent( " on " + type, fn);
- }
- }
- }
- // Kill off the registry itself to get rid of the last remaining
- // references.
- EventManager._registry = null ;
- }
- };
使用起来也很简单
- <html>
- <head>
- <script type=text/javascript src=EventManager.js></script>
- <script type=text/javascript>
- function onLoad() {
- EventManager.Add(document.getElementById(testCase),click,hit );
- returntrue;
- }
- function hit(evt) {
- alert(click);
- }
- </script>
- </head>
- <body onload='javascript: onLoad();'>
- <div id='testCase' style='width:100%; height: 100%; yellow;'>
- <h1>Click me!</h1>
- </div>
- </body>
- </html>
google map api同样提供了一个类似的函数用在页面的unload事件中,解决Closure带来的内存泄露问题。
当然,如果你不嫌麻烦,你也可以为每个和native object有关的就阿vascript object编写一个destoryMemory函数,用来手动调用,从而手动解除Dom对象的事件绑定。
还有一种就是不要那么OO,抛弃Dom的一些特性,用innerHTML代替appendChild,避开循环引用。详细见http://birdshome.cnblogs.com/archive/2005/02/16/104967.html中的讨论贴。
Cross-Page Leaks
Cross-Page Leaks和下一节提到的Pseudo-Leaks在我看来,就是IE的bug, 虽然MS死皮赖脸不承认:)
大家可以看看这段例子代码:
- < html >
- < head >
- < script language = " JScript " >
- function LeakMemory() // 这个函数会引发Cross-Page Leaks
- {
- var hostElement = document.getElementById( " hostElement " );
- // Do it a lot, look at Task Manager for memory response
- for (i = 0 ; i < 5000 ; i ++ )
- {
- var parentDiv =
- document.createElement( " <div onClick='foo()'> " );
- var childDiv =
- document.createElement( " <div onClick='foo()'> " );
- // This will leak a temporary object
- parentDiv.appendChild(childDiv);
- hostElement.appendChild(parentDiv);
- hostElement.removeChild(parentDiv);
- parentDiv.removeChild(childDiv);
- parentDiv = null ;
- childDiv = null ;
- }
- hostElement = null ;
- }
- function CleanMemory() // 而这个函数不会引发Cross-Page Leaks
- {
- var hostElement = document.getElementById( " hostElement " );
- // Do it a lot, look at Task Manager for memory response
- for (i = 0 ; i < 5000 ; i ++ )
- {
- var parentDiv = document.createElement( " <div onClick='foo()'> " );
- var childDiv = document.createElement( " <div onClick='foo()'> " );
- // Changing the order is important, this won't leak
- hostElement.appendChild(parentDiv);
- parentDiv.appendChild(childDiv);
- hostElement.removeChild(parentDiv);
- parentDiv.removeChild(childDiv);
- parentDiv = null ;
- childDiv = null ;
- }
- hostElement = null ;
- }
- </ script >
- </ head >
- < body >
- < button onclick = " LeakMemory() " > Memory Leaking Insert </ button >
- < button onclick = " CleanMemory() " > Clean Insert </ button >
- < div id = " hostElement " ></ div >
- </ body >
- </ html >
LeakMemory和CleanMemory这两段函数的唯一区别就在于他们的代码的循序,从代码上看,两段代码的逻辑都没有错。
但LeakMemory却会造成泄露。原因是LeakMemory()会先建立起parentDiv和childDiv之间的连接,这时候,为了让 childDiv能够获知parentDiv的信息,因此IE需要先建立一个临时的scope对象。而后parentDiv建立了和 hostElement对象的联系,parentDiv和childDiv直接使用页面document的scope。可惜的是,IE不会释放刚才那个临 时的scope对象的内存空间,直到我们跳转页面,这块空间才能被释放。而CleanMemory函数不同,他先把parentDiv和 hostElement建立联系,而后再把childDiv和parentDiv建立联系,这个过程不需要单独建立临时的scope,只要直接使用页面 document的scope就可以了, 所以也就不会造成内存泄露了
详细原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp这篇文章。
btw:
IE 6中垃圾回收算法,就是从那些直接"in scope"的对象开始进行mark清除的:
Every variable which is "in scope" is called a "scavenger". A scavenger may refer to a number, an object, a string, whatever. We maintain a list of scavengers – variables are moved on to the scav list when they come into scope and off the scav list when they go out of scope.
Pseudo-Leaks
这个被称为“秀逗泄露”真是恰当啊:)
看看这个例子:
- < html >
- < head >
- < script language = " JScript " >
- function LeakMemory()
- {
- // Do it a lot, look at Task Manager for memory response
- for (i = 0 ; i < 5000 ; i ++ )
- {
- hostElement.text = " function foo() { } " ;//看内存会不断增加
- }
- }
- </ script >
- </ head >
- < body >
- < button onclick = " LeakMemory() " > Memory Leaking Insert </ button >
- < script id = " hostElement " > function foo() { } </ script >
- </ body >
- </ html >
MS是这么解释的,这不是内存泄漏。如果您创建了许多无法获得也无法释放的对象,那才是内存泄漏。在这里,您将创建许多元素,Internet Explorer 需要保存它们以正确呈现页面。Internet Explorer 并不知道您以后不会运行操纵您刚刚创建的所有这些对象的脚本。当页面消失时(当您浏览完,离开浏览器时)会释放内存。它不会泄漏。当销毁页面时,会中断循 环引用。
唉~~~
详细原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp这篇文章。
其它一些琐碎的注意点
变量定义一定要用var,否则隐式声明出来的变量都是全局变量,不是局部变量;
全局变量没用时记得要置null;
注意正确使用delete,删除没用的一些函数属性;
注意正确使用try...cache,确保去处无效引用的代码能被正确执行;
open出来的窗口即使close了,它的window对象还是存在的,要记得删除引用;
frame和iframe的情况和窗口的情况类似。
参考资料
http://jibbering.com/faq/faq_notes/closures.html
http://javascript.weblogsinc.com/2005/03/07/javascript-memory-leaks/
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp
http://72.14.203.104/search?q=cache:V9Bt4_HBzQ8J:jgwebber.blogspot.com/2005/01/dhtml-leaks-like-sieve.html+DHTML+Leaks+Like+a+Sieve+&hl=zh-CN&ct=clnk&cd=9 (这是DHTML Leaks Like a Sieve)一文在google上的cache,原文已经连不上了)
http://spaces.msn.com/siteexperts/Blog/cns!1pNcL8JwTfkkjv4gg6LkVCpw!338.entry
http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555
http://www.ajaxtopics.com/leakpatterns.html
http://blogs.msdn.com/ericlippert/archive/2003/09/17/53028.aspx
http://www.quirksmode.org/blog/archives/2005/02/javascript_memo.html
http://youngpup.net/2005/0221010713
http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx =
http://support.microsoft.com/kb/266071/EN-US ==>IE 5.0至5.5一些版本中的GC bug
http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html ==>ie 7的改进
http://erik.eae.net/archives/2006/04/26/23.23.02/ ==>ie 7的改进
http://www.feedbackarchive.com/spamvampire/today.html ==> Try this script for memory leaks - it leaked 50 megabytes in 15 minutes with firefox on linux:
http://birdshome.cnblogs.com/archive/2005/02/15/104599.html
http://www.quirksmode.org/dom/innerhtml.html
http://www.crockford.com/javascript/memory/leak.html
《JavaScript: The Definitive Guide》4th Edition
http://outofhanwell.com/ieleak/index.php?title=Main_Page
关于Javascript的内存泄漏问题的整理稿的更多相关文章
- Javascript的内存泄漏分析
作为程序员(更高大尚的称谓:研软件研发)的我们,无论是用Javascript,还是.net, java语言,肯定都遇到过内存泄漏的问题.只不过他们都有GC机制来帮助程序员完成内存回收的事情,如果你是C ...
- [BUGCASE]Webpack打包报JavaScript堆内存泄漏的错误
一.问题描述 执行npm run build之后报错: 报错信息: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript he ...
- 关于JavaScript内存泄漏的质疑
近几天看了些关于JavaScript内存管理的文章,相对于Java JVM的内存管理,显得简单些. 在学习的过程中,发现有不少网友谈到了循环引用,说循环引用会造成内存泄漏,垃圾回收器无法回收. 实际上 ...
- JavaScript的闭包和内存泄漏问题
闭包 http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html JavaScript中必须提到的功能最强大的抽象 ...
- 了解 JavaScript 应用程序中的内存泄漏
简介 当处理 JavaScript 这样的脚本语言时,很容易忘记每个对象.类.字符串.数字和方法都需要分配和保留内存.语言和运行时的垃圾回收器隐藏了内存分配和释放的具体细节. 许多功能无需考虑内存管理 ...
- JavaScript 中的内存泄漏
JavaScript 中的内存泄漏 JavaScript 是一种垃圾收集式语言,这就是说,内存是根据对象的创建分配给该对象的,并会在没有对该对象的引用时由浏览器收回.JavaScript 的垃圾收集机 ...
- 一个意想不到的Javascript内存泄漏
原文:http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html 本周我在Meter的同事追踪到了一 ...
- Javascript内存泄漏
Javascript内存泄漏 原文:http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html 本周 ...
- JavaScript中的内存泄漏以及如何处理
随着现在的编程语言功能越来越成熟.复杂,内存管理也容易被大家忽略.本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题. 概 ...
随机推荐
- 北邮网关登录python脚本
闲来无聊,来码一发 安装 pip install byrlogin 登录 登出
- html中的div、td 、p 等容器内强制换行和不换行的实现
div.td .p 等容器内强制换行和不换行,在某些情况下还是比较实用的,下面本文整理了一些相关方面的知识,并有具体的实现方法,需要的朋友可以参考下1.强制不换行,同时以省略号结尾. 代码如下:< ...
- bzoj 2038 [2009国家集训队]小Z的袜子(hose)(莫队算法)
[题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=2038 [题意] 给定一个有颜色的序列,回答若干个询问:区间内任选两个颜色相同的概率. ...
- ps做gif 登陆下拉菜单效果
作者这里仅介绍登录动画的制作思路和简单过程.一些细节的制作,如登录框,每一帧的图像等都需要自己根据参考图慢慢完成.最终效果 1.新建大小适当的文件,背景填充暗蓝色.首先设计一个底座,主要用图层样式来完 ...
- Python:数字
一.数字简介 数字可以直接访问,是不可更改并且不可分割的原子类型,这些在标准类型的分类中都谈到了.不可更改意味着变更数字值的实质是新对象的创建.当然,这些对于程序员来说都是透明的,不需过多考虑. 1. ...
- bzoj3714: [PA2014]Kuglarz
[PA2014]KuglarzTime Limit: 20 Sec Memory Limit: 128 MBSubmit: 553 Solved: 317[Submit][Status][Discus ...
- ssh-keygen -t rsa -f cloud.key ssh -i cloud.key <username>@<instance_ip>
- 转】腾讯云CentOS 6.6安装 Nginx
原博文出自于: http://www.cnblogs.com/xdp-gacl/p/5290155.html 感谢! 一.下载Nginx 从Nginx的官网(http://nginx.org/en/d ...
- 【MySql】性能优化之分析命令
一 当发现程序运行比较慢的时候,首先排除物力资源问题之后,就将注意力转向mysq数据库: 1.首先确定运行慢的sql语句: mysql> show full processlist; 2.确认低 ...
- iconv 的参数问题
工作中遇到一个转码的问题,如下代码说话 void encode_convert(iconv_t& cd, const char* str, size_t str_len, std::strin ...