JS哪些操作会js哪些造成内存泄漏漏

Javascript内存泄漏 - 文章 - 伯乐在线
& Javascript内存泄漏
1.什么是内存泄漏?
内存泄漏是指分配给应用的内存不能被重新分配,即使在内存已经不被使用的时候。正常情况下,垃圾回收器在DOM元素和event处理器不被引用或访问的时候回收它们。但是,IE的早些版本(IE7和之前)中内存泄漏是很容易出现的,因为内存管理器不能正确理解Javascript生命周期而且在周期被打破(可以通过赋值为null实现)前不会回收内存。
2.为什么你需要注意它?
在大型Web应用程序中内存泄漏是一种常见的意外编程错误。内存泄漏会降低Web应用程序的性能,直到浪费的内存超过了系统所能分配的,应用程序将不能使用。作为一Web开发者,开发一个满足功能要求的应用程序只是第一步,性能要求和Web应用程序的成功是同样重要的,更何况它可能会导致应用程序错误或浏览器崩溃。
3.Javascript中出现内存泄漏的主要原因是什么?
1)循环引用
一个很简单的例子:一个DOM对象被一个Javascript对象引用,与此同时又引用同一个或其它的Javascript对象,这个DOM对象可能会引发内存泄漏。这个DOM对象的引用将不会在脚本停止的时候被垃圾回收器回收。要想破坏循环引用,引用DOM元素的对象或DOM对象的引用需要被赋值为null。
2)Javascript闭包
因为Javascript范围的限制,许多实现依赖Javascript不包,请查看我的前面的文章如果你想了解更多闭包方面的问题。
闭包可以导致内存泄漏是因为内部方法保持一个对外部方法变量的引用,所以尽管方法返回了内部方法还可以继续访问在外部方法中定义的私有变量。对Javascript程序员来说最好的做法是在页面重载前断开所有的事件处理器。
3)DOM插入顺序
当2个不同范围的 DOM 对象连添加到一起的时候一个临时的对象会被创建。这个DOM对象改变范围到document时,那个临时对象就没用了。也就是说, DOM 对象应该按照从当前页面存在的最上面的 DOM 元素开始往下直到剩下的 DOM 元素的顺序添加,这样它们就总是有同样的范围,不会产生临时对象。
4)如何检测?
内存泄漏对开发者来说一般很难检测因为它们是由一些大量代码中的意外的错误引起的,但它在系统内存不足前并不影响程序的功能。这就是为什么会有人在很长时间的测试期中收集应用程序性能指标来测试性能。
最简单的检测内存泄漏的方式是用任务管理器检查内存使用情况。在Chrome浏览器的新选项卡中打开应用并查看内存使用量是不是越来越多。还有其他的调试工具提供内存监视器,比如Chrome开发者工具。这是谷歌开者这网站中的的特性的教程。
1. /memory/leak.html
2. /en-us/library/Bb250448
3. /developerworks/web/library/wa-memleak/
更新: 23:27
此文摘要发布至微博后,: “ 其实1和2是同一个原因,而且此文漏掉一种情况,老朽很久前写过一篇旧文 ”,如下。 */
什么是内存泄露
内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。在C++中,因为是手动管理内存,内存泄露是经常出现的事情。而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。
内存泄露Quick View
不同的浏览器中存在各种内存泄露方式,目前发现的主要是这样几种:
已经确认存在泄漏的浏览器:IE6.0 FF2.0
含有DOM对象的循环引用将导致大部分当前主流浏览器内存泄露 这里有两个简单的概念
引用:a.属性=b,a就引用了b
循环引用:简单来说假如a引用了b,b又引用了a,a和b就构成了循环引用。
a和b循环引用:
JavaScript
var a=new O
var b=new O
var a=new Object;var b=new Object;a.r=b;b.r=a;
a循环引用自己:
var a=new O
var a=new Object;a.r=a;
循环引用很常见且大部分情况下是无害的,但当参与循环引用的对象中有DOM对象或者ActiveX对象时,循环引用将导致内存泄露。我们把例子中的任何一个new Object替换成document.getElementById或者document.createElement就会发生内存泄露了。
尽管这看起来非常容易理解,但是因为有closure的参与而使事情变得复杂,有些closure导致的循环引用很难被察觉。下面是一个非常常见的动态绑定事件:
JavaScript
function bindEvent()
var obj=document.createElement("XXX");
obj.onclick=function(){
//Even if it's a empty function
function bindEvent(){&&&&var obj=document.createElement("XXX");&&&&obj.onclick=function(){&&&&&&&&//Even if it's a empty function&&&&}}
这个bindEvent执行时100%会发生内存泄露,Someone 可能会问,哪里出现了循环引用? 关于closure和scope chain参与的循环引用比较复杂,此处暂不深入讨论。有一个简单的判断方式:函数将间接引用所有它能访问的对象。obj.onclick这个函数中 可以访问外部的变量obj 所以他引用了obj,而obj又引用了它,因此这个事件绑定将会造成内存泄露。在中介绍了2种方式解决类似的问题一个是obj=null,另一个是把onclick的函数写在bindEvent外,重复人家的我就不说了。简单贴下代码:
JavaScript
function bindEvent()
var obj=document.createElement("XXX");
obj.onclick=onclickH
function onclickHandler(){
//do something
function bindEvent(){&&&&var obj=document.createElement("XXX");&&&&obj.onclick=onclickHandler;}function onclickHandler(){&&&&//do something}
JavaScript
function bindEvent()
var obj=document.createElement("XXX");
obj.onclick=function(){
//Even if it's a empty function
function bindEvent(){&&&&var obj=document.createElement("XXX");&&&&obj.onclick=function(){&&&&&&&&//Even if it's a empty function&&&&}&&&&obj=null;}
这两个方法都打断了循环引用,可以解决问题,但是似乎对代码表达能力造成了一定破坏,假设有这么一个问题:
JavaScript
function bindEvent()
var obj=document.createElement("XXX");
var var0="OOXX";//Here is a variable
obj.onclick=function(){
alert(var0);//I want to visit var2 here!
//bindEvent must return obj!
function bindEvent(){&&&&var obj=document.createElement("XXX");&&&&var var0="OOXX";//Here is a variable&&&&obj.onclick=function(){&&&&&&&&alert(var0);//I want to visit var2 here!&&&&}&&&&return obj;//bindEvent must return obj!}
好了,这下两种办法都不行了,假如我把函数写外面去,var0肯定访问不了,假如我把obj弄成null,还怎么return它呢?这并不是空想的需要,这实际 上是一个用JS定制DOM控件的简单抽象:创建DOM元素、设置私有属性、绑定事件。所以,我们必须update一下两个方法。首先,方法1,为了让函数 能访问某些变量,我们可以通过一个Builder函数来订制onclick的外部闭包:
JavaScript
function bindEvent()
var obj=document.createElement("XXX");
var var0="OOXX";//Here is a variable
obj.onclick= onclickBuilder(var0);//想访问谁就把谁传进去!!
//bindEvent must return obj!
function onclickBuilder(var0)//这里跟上面对应上就行了 最好参数名字也对应上
return function(){
alert(var0);
12345678910111213
function bindEvent(){&&&&var obj=document.createElement("XXX");&&&&var var0="OOXX";//Here is a variable&&&&obj.onclick= onclickBuilder(var0);//想访问谁就把谁传进去!!&&&&return obj;//bindEvent must return obj!}function onclickBuilder(var0)//这里跟上面对应上就行了 最好参数名字也对应上{&&&&return function(){&&&&&&&&alert(var0);&&&&}}
第二个办法,这个来自51js的chpn同学,让obj=null在return 之后执行!!
JavaScript
function bindEvent()
var obj=document.createElement("XXX");
var var0="OOXX";//Here is a variable
obj.onclick=function(){
alert(var0);//I want to visit var2 here!
//bindEvent must return obj!
} finally {
12345678910111213
function bindEvent(){&&&&try{&&&&&&&&var obj=document.createElement("XXX");&&&&&&&&var var0="OOXX";//Here is a variable&&&&&&&&obj.onclick=function(){&&&&&&&&&&&&alert(var0);//I want to visit var2 here!&&&&&&&&}&&&&&&&&return obj;//bindEvent must return obj!&&&&} finally {&&&&&&&&obj=null;&&&&}}
某些DOM操作
这是IE系列的特有问题 简单的来说就是在向不在DOM树上的DOM元素appendChild,可能会发生内存泄露(只是可能,具体原因不明,似乎十分复杂,下面例子中去掉onClick也可以避免泄露)。所以appendChild的顺序可能影响内存泄露,来自微软的例子:
&script language="JScript"&
function LeakMemory()
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 =
childDiv =
hostElement =
function CleanMemory()
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 =
childDiv =
hostElement =
&button onclick="LeakMemory()"&Memory Leaking Insert&/button&
&button onclick="CleanMemory()"&Clean Insert&/button&
&div id="hostElement"&&/div&
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
&/html&&&&&&&head&&&&&&&&&&&script language="JScript"&&&&&&&&&&function LeakMemory()&&&&&&&&&{&&&&&&&&&&&&&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()&&&&&&&&&{&&&&&&&&&&&&&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&
而在IE7中,貌似为了改善内存泄露,IE7采用了极端的解决方案:离开页面时回收所有DOM树上的元素,其它一概不管。但是这不仅没起到任何作用,反而 使问题变得更加复杂。对这类问题,除了自觉一点绕开这些恶心的东西,多用innerHTML这种无用的建议之外。我想可以通过覆盖 document.createElement来略为改善:
首先我们定义一个看不见的元素当作垃圾箱,所有新创建的元素都扔进垃圾箱里,这样保证了所有DOM元素都在DOM树上,IE7就可以正确回收了,另一方面也能避免所谓的”appendChild顺序不对导致内存泄露”。
JavaScript
function MemoryFix(){
var garbageBox=document.createElement("div");
garbageBox.style.display="none";
document.body.appendChild(garbageBox);
var createElement=document.createE
document.createElement=function(){
var obj=Function.prototype.apply.apply(createElement,[document,arguments]);
garbageBox.appendChild(obj);
123456789101112131415161718192021
function MemoryFix(){&&&&&var garbageBox=document.createElement("div");&&&&&garbageBox.style.display="none";&&&&&document.body.appendChild(garbageBox);&&&&&var createElement=document.createElement;&&&&&document.createElement=function(){&&&&&&&&&var obj=Function.prototype.apply.apply(createElement,[document,arguments]);&&&&&&&&&garbageBox.appendChild(obj);&&&&&&&&&return obj;&&&&&}&}
自动类型装箱转换
别不相信,下面的代码在ie系列中会导致内存泄露
JavaScript
var s=”lalala”;
alert(s.length);
var s=”lalala”;alert(s.length);
s本身是一个string而非object,它没有length属性,所以当访问length时,JS引擎会自动创建一个临时String对象封装s,而这个对象一定会泄露。这个bug匪夷所思,所幸解决起来相当容易,记得所有值类型做.运算之前先显式转换一下:
JavaScript
var s="lalala";
alert(new String(s).length);
var s="lalala";alert(new String(s).length);
寒冬的原文:浏览器中的内存泄露
可能感兴趣的话题
关于伯乐在线博客
在这个信息爆炸的时代,人们已然被大量、快速并且简短的信息所包围。然而,我们相信:过多“快餐”式的阅读只会令人“虚胖”,缺乏实质的内涵。伯乐在线内容团队正试图以我们微薄的力量,把优秀的原创文章和译文分享给读者,为“快餐”添加一些“营养”元素。
新浪微博:
推荐微信号
(加好友请注明来意)
– 好的话题、有启发的回复、值得信赖的圈子
– 分享和发现有价值的内容与观点
– 为IT单身男女服务的征婚传播平台
– 优秀的工具资源导航
– 翻译传播优秀的外文文章
– 国内外的精选文章
– UI,网页,交互和用户体验
– 专注iOS技术分享
– 专注Android技术分享
– JavaScript, HTML5, CSS
– 专注Java技术分享
– 专注Python技术分享
& 2017 伯乐在线在 SegmentFault,解决技术问题
每个月,我们帮助 1000 万的开发者解决各种各样的技术问题。并助力他们在技术能力、职业生涯、影响力上获得提升。
一线的工程师、著名开源项目的作者们,都在这里:
获取验证码
已有账号?
问题对人有帮助,内容完整,我也想知道答案
问题没有实际价值,缺少关键内容,没有改进余地
近期做前端页面,使用了大量的插件和各种UI框架,尤其是做web app 页面的响应速度大大的降低了!请问该如何防止页面响应时间过长呢?除了用r.js压缩JS外,如何规范的对dom的操作和建立合理的内存回收的机制,提高JS的性能呢?(不要复制百度搜来的,谢谢!)
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
因资源请求造成的性能问题,需要:1.资源压缩、合并;2.减少请求数;3.CDN分发。
因页面JS引起的性能问题,需要:1.了解重排、重绘造成性能问题及解决方案;2.涉及大量字符串操作时,使用array的join,而不是字符串拼接;3.其他的优化点:
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
用原生js写,少用第三方框架类,用amd模块加载
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
想要性能就别上什么UI框架,除非能确定它能把事件都解绑了,譬如我自己的。有事件的组件都提供了 unbind 或者 remove 方法,需要销毁的时候调用一下就行了
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
web app 不适合做交互很复杂的应用,你会发现即使不用控件。很简单的单选或者下拉框在一些低配的手机里都会响应慢,更何况一个页面上有若干个控件。所以现在国内著名app都很少有用web app的。web app就是适合做浏览用。做内嵌效果不错。
同步到新浪微博
分享到微博?
关闭理由:
删除理由:
忽略理由:
推广(招聘、广告、SEO 等)方面的内容
与已有问题重复(请编辑该提问指向已有相同问题)
答非所问,不符合答题要求
宜作评论而非答案
带有人身攻击、辱骂、仇恨等违反条款的内容
无法获得确切结果的问题
非开发直接相关的问题
非技术提问的讨论型问题
其他原因(请补充说明)
我要该,理由是:你的位置:
> JavaScript 中 4 种常见的内存泄露陷阱
JavaScript 中 4 种常见的内存泄露陷阱
发表于( 20:54) 本文标签:
浏览量:65次
&&&&值班编辑QQ:
(点击上方公众号,可快速关注)
原文:Sebastián Peyrott
译文:伯乐在线专栏作者 - ARIGATO
链接:/88463/
点击 → 了解如何加入专栏作者
了解 Java 的内存泄露和解决方式!
在这篇文章中我们将要探索客户端 Java 代码中常见的一些内存泄漏的情况,并且学习如何使用 Chrome 的开发工具来发现他们。读一读吧!
内存泄露是每个开发者最终都不得不面对的问题。即便使用自动内存管理的语言,你还是会碰到一些内存泄漏的情况。内存泄露会导致一系列问题,比如:运行缓慢,崩溃,高延迟,甚至一些与其他应用相关的问题。
什么是内存泄漏
本质上来讲,内存泄露是当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者空闲内存池的现象。编程语言使用不同的方式来管理内存。这些方式可能会减少内存泄露的机会。然而,某一块具体的内存是否被使用实际上是一个不可判定问题(undecidable problem)。换句话说,只有开发者可以搞清楚一块内存是否应该被操作系统回收。某些编程语言提供了帮助开发者来处理这件事情的特性。而其它的编程语言需要开发者明确知道内存的使用情况。维基百科上有几篇写的不错的讲述手动 和自动内存管理的文章。
Java 的内存管理
Java 是那些被称作垃圾回收语言当中的一员。垃圾回收语言通过周期性地检查那些之前被分配出去的内存是否可以从应用的其他部分访问来帮助开发者管理内存。换句话说,垃圾回收语言将内存管理的问题从“什么样的内存是仍然被使用的?”简化成为“什么样的内存仍然可以从应用程序的其他部分访问?”。两者的区别是细微的,但是很重要:开发者只需要知道一块已分配的内存是否会在将来被使用,而不可访问的内存可以通过算法确定并标记以便返还给操作系统。
非垃圾回收语言通常使用其他的技术来管理内存,包括:显式内存管理,程序员显式地告诉编译器在何时不再需要某块内存;引用计数,一个计数器关联着每个内存块(当计数器的计数变为0的时候,这块内存就被操作系统回收)。这些技术都有它们的折中考虑(也就是说都有潜在的内存泄漏风险)。
Java 中的内存泄露
引起垃圾收集语言内存泄露的主要原因是不必要的引用。想要理解什么是不必要的引用,首先我们需要理解垃圾收集器是怎样确定一块内存能否被访问的。
Mark-and-sweep
大多数的垃圾收集器(简称 GC)使用一个叫做 mark-and-sweep 的算法。这个算法由以下的几个步骤组成:
垃圾收集器建立了一个“根节点”列表。根节点通常是那些引用被保留在代码中的全局变量。对于 Java 而言,“Window” 对象就是一个能作为根节点的全局变量例子。window 对象是一直都存在的(即:不是垃圾)。所有根节点都是检查过的并且被标记为活动的(即:不是垃圾)。所有的子节点也都被递归地检查过。每块可以从根节点访问的内存都不会被视为垃圾。 所有没有被标记为垃圾的内存现在可以被当做垃圾,而垃圾收集器也可以释放这些内存并将它们返还给操作系统。现代垃圾收集器使用不同的方式来改进这些算法,但是它们都有相同的本质:可以访问的内存块被标记为非垃圾而其余的就被视为垃圾。
不必要的引用就是那些程序员知道这块内存已经没用了,但是出于某种原因这块内存依然存在于活跃的根节点发出的节点树中。在 Java 的环境中,不必要的引用是某些不再被使用的代码中的变量。这些变量指向了一块本来可以被释放的内存。一些人认为这是程序员的失误。
所以想要理解什么是 Java 中最常见的内存泄露,我们需要知道在什么情况下会出现不必要的引用。
3 种常见的 Java 内存泄露 1: 意外的全局变量
Java 语言的设计目标之一是开发一种类似于 Java 但是对初学者十分友好的语言。体现 Java 宽容性的一点表现在它处理未声明变量的方式上:一个未声明变量的引用会在全局对象中创建一个新的变量。在浏览器的环境下,全局对象就是 window,也就是说:
functionfoo(arg){
bar= "this is a hidden global variable";
实际上是:
functionfoo(arg){
window.bar= "this is an explicit global variable";
如果 bar 是一个应该指向 foo 函数作用域内变量的引用,但是你忘记使用 var 来声明这个变量,这时一个全局变量就会被创建出来。在这个例子中,一个简单的字符串泄露并不会造成很大的危害,但这无疑是错误的。
另外一种偶然创建全局变量的方式如下:
functionfoo(){
this.variable= "potential accidental global";
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
// 函数自身发生了调用,this 指向全局对象(window),(译者注:这时候会为全局对象 window 添加一个 variable 属性)而不是 undefined。
为了防止这种错误的发生,可以在你的 Java 文件开头添加 'use strict'; 语句。这个语句实际上开启了解释 Java 代码的严格模式,这种模式可以避免创建意外的全局变量。
全局变量的注意事项
尽管我们在讨论那些隐蔽的全局变量,但是也有很多代码被明确的全局变量污染的情况。按照定义来讲,这些都是不会被回收的变量(除非设置 null 或者被重新赋值)。特别需要注意的是那些被用来临时存储和处理一些大量的信息的全局变量。如果你必须使用全局变量来存储很多的数据,请确保在使用过后将它设置为 null 或者将它重新赋值。常见的和全局变量相关的引发内存消耗增长的原因就是缓存。缓存存储着可复用的数据。为了让这种做法更高效,必须为缓存的容量规定一个上界。由于缓存不能被及时回收的缘故,缓存无限制地增长会导致很高的内存消耗。
2: 被遗漏的定时器和回调函数
在 Java 中 setInterval 的使用十分常见。其他的库也经常会提供观察者和其他需要回调的功能。这些库中的绝大部分都会关注一点,就是当它们本身的实例被销毁之前销毁所有指向回调的引用。在 setInterval 这种情况下,一般情况下的代码是这样的:
varsomeResource= getData();
setInterval(function(){
varnode= document.getElementById('Node');
// Do stuff with node and someResource.
node.innerHTML= JSON.stringify(someResource));
这个例子说明了摇晃的定时器会发生什么:引用节点或者数据的定时器已经没用了。那些表示节点的对象在将来可能会被移除掉,所以将整个代码块放在周期处理函数中并不是必要的。然而,由于周期函数一直在运行,处理函数并不会被回收(只有周期函数停止运行之后才开始回收内存)。如果周期处理函数不能被回收,它的依赖程序也同样无法被回收。这意味着一些资源,也许是一些相当大的数据都也无法被回收。
下面举一个观察者的例子,当它们不再被需要的时候(或者关联对象将要失效的时候)显式地将他们移除是十分重要的。在以前,尤其是对于某些浏览器(IE6)是一个至关重要的步骤,因为它们不能很好地管理循环引用(下面的代码描述了更多的细节)。现在,当观察者对象失效的时候便会被回收,即便 listener 没有被明确地移除,绝大多数的浏览器可以或者将会支持这个特性。尽管如此,在对象被销毁之前移除观察者依然是一个好的实践。示例如下:
varelement= document.getElementById('button');
functiononClick(event){
element.innerHtml= 'text';
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 even in old browsers that don't
// handle cycles well.
对象观察者和循环引用中一些需要注意的点
观察者和循环引用常常会让 Java 开发者踩坑。以前在 IE 浏览器的垃圾回收器上会导致一个 bug(或者说是浏览器设计上的问题)。旧版本的 IE 浏览器不会发现 DOM 节点和 Java 代码之间的循环引用。这是一种观察者的典型情况,观察者通常保留着一个被观察者的引用(正如上述例子中描述的那样)。换句话说,在 IE 浏览器中,每当一个观察者被添加到一个节点上时,就会发生一次内存泄漏。这也就是开发者在节点或者空的引用被添加到观察者中之前显式移除处理方法的原因。目前,现代的浏览器(包括 IE 和 Microsoft Edge)都使用了可以发现这些循环引用并正确的处理它们的现代化垃圾回收算法。换言之,严格地讲,在废弃一个节点之前调用 removeEventListener 不再是必要的操作。
像是 jQuery 这样的框架和库(当使用一些特定的 API 时候)都在废弃一个结点之前移除了 listener 。它们在内部就已经处理了这些事情,并且保证不会产生内存泄露,即便程序运行在那些问题很多的浏览器中,比如老版本的 IE。
3: DOM 之外的引用
有些情况下将 DOM 结点存储到数据结构中会十分有用。假设你想要快速地更新一个表格中的几行,如果你把每一行的引用都存储在一个字典或者数组里面会起到很大作用。如果你这么做了,程序中将会保留同一个结点的两个引用:一个引用存在于 DOM 树中,另一个被保留在字典中。如果在未来的某个时刻你决定要将这些行移除,则需要将所有的引用清除。
varelements= {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
functiondoStuff(){
image.src= 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
functionremoveButton(){
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
还需要考虑另一种情况,就是对 DOM 树子节点的引用。假设你在 Java 代码中保留了一个表格中特定单元格(一个
标签)的引用。在将来你决定将这个表格从 DOM 中移除,但是仍旧保留这个单元格的引用。凭直觉,你可能会认为 GC 会回收除了这个单元格之外所有的东西,但是实际上这并不会发生:单元格是表格的一个子节点且所有子节点都保留着它们父节点的引用。换句话说,Java 代码中对单元格的引用导致整个表格被保留在内存中。所以当你想要保留 DOM 元素的引用时,要仔细的考虑清除这一点。
Java 开发中一个重要的内容就是闭包,它是可以获取父级作用域的匿名函数。Meteor 的开发者发现在一种特殊情况下有可能会以一种很微妙的方式产生内存泄漏,这取决于 Java 运行时的实现细节。
vartheThing=
varreplaceThing= function(){
varoriginalThing= theT
varunused= function(){
if(originalThing)
console.log("hi");
theThing= {
longStr: newArray(1000000).join('*'),
someMethod: function(){
console.log(someMessage);
setInterval(replaceThing,1000);
这段代码做了一件事:每次调用 replaceThing 时,theThing 都会得到新的包含一个大数组和新的闭包(someMethod)的对象。同时,没有用到的那个变量持有一个引用了 originalThing(replaceThing 调用之前的 theThing)闭包。哈,是不是已经有点晕了?关键的问题是每当在同一个父作用域下创建闭包作用域的时候,这个作用域是被共享的。在这种情况下,someMethod 的闭包作用域和 unused 的作用域是共享的。unused 持有一个 originalThing 的引用。尽管 unused 从来没有被使用过,someMethod 可以在 theThing 之外被访问。而且 someMethod 和 unused 共享了闭包作用域,即便 unused 从来都没有被使用过,它对 originalThing 的引用还是强制它保持活跃状态(阻止它被回收)。当这段代码重复运行时,将可以观察到内存消耗稳定地上涨,并且不会因为 GC 的存在而下降。本质上来讲,创建了一个闭包链表(根节点是 theThing 形式的变量),而且每个闭包作用域都持有一个对大数组的间接引用,这导致了一个巨大的内存泄露。
这是一种人为的实现方式。可以想到一个能够解决这个问题的不同的闭包实现,就像 Metero 的博客里面说的那样。
垃圾收集器的直观行为
尽管垃圾收集器是便利的,但是使用它们也需要有一些利弊权衡。其中之一就是不确定性。也就是说,GC 的行为是不可预测的。通常情况下都不能确定什么时候会发生垃圾回收。这意味着在一些情形下,程序会使用比实际需要更多的内存。有些的情况下,在很敏感的应用中可以观察到明显的卡顿。尽管不确定性意味着你无法确定什么时候垃圾回收会发生,不过绝大多数的 GC 实现都会在内存分配时遵从通用的垃圾回收过程模式。如果没有内存分配发生,大部分的 GC 都会保持静默。考虑以下的情形:
大量内存分配发生时。
大部分(或者全部)的元素都被标记为不可达(假设我们讲一个指向无用缓存的引用置 null 的时候)。
没有进一步的内存分配发生。
这个情形下,GC 将不会运行任何进一步的回收过程。也就是说,尽管有不可达的引用可以触发回收,但是收集器并不要求回收它们。严格的说这些不是内存泄露,但仍然导致高于正常情况的内存空间使用。
Google 在它们的 Java 内存分析文档中提供一个关于这个行为的优秀例子,见示例#2.
Chrome 内存分析工具简介
Chrome 提供了一套很好的工具用来分析 Java 的内存适用。这里有两个与内存相关的重要视图:timeline 视图和 profiles 视图。
Timeline view
timeline 视图是我们用于发现不正常内存模式的必要工具。当我们寻找严重的内存泄漏时,内存回收发生后产生的周期性的不会消减的内存跳跃式增长会被一面红旗标记。在这个截图里面我们可以看到,这很像是一个稳定的对象内存泄露。即便最后经历了一个很大的内存回收,它占用的内存依旧比开始时多得多。节点数也比开始要高。这些都是代码中某处 DOM 节点内存泄露的标志。
Profiles 视图
你将会花费大部分的时间在观察这个视图上。profiles 视图让你可以对 Java 代码运行时的内存进行快照,并且可以比较这些内存快照。它还让你可以记录一段时间内的内存分配情况。在每一个结果视图中都可以展示不同类型的列表,但是对我们的任务最有用的是 summary 列表和 comparison 列表。
summary 视图提供了不同类型的分配对象以及它们的合计大小:shallow size (一个特定类型的所有对象的总和)和 retained size (shallow size 加上保留此对象的其它对象的大小)。distance 显示了对象到达 GC 根(校者注:最初引用的那块内存,具体内容可自行搜索该术语)的最短距离。
comparison 视图提供了同样的信息但是允许对比不同的快照。这对于找到泄露很有帮助。
举例: 使用 Chrome 来发现内存泄露
有两个重要类型的内存泄露:引起内存周期性增长的泄露和只发生一次且不引起更进一步内存增长的泄露。显而易见的是,寻找周期性的内存泄漏是更简单的。这些也是最麻烦的事情:如果内存会按时增长,泄露最终将导致浏览器变慢或者停止执行脚本。很明显的非周期性大量内存泄露可以很容易的在其他内存分配中被发现。但是实际情况并不如此,往往这些泄露都是不足以引起注意的。这种情况下,小的非周期性内存泄露可以被当做一个优化点。然而那些周期性的内存泄露应该被视为 bug 并且必须被修复。
为了举例,我们将会使用 Chrome 的文档中提供的一个例子。完整的代码在下面可以找到:
functioncreateSomeNodes(){
frag= document.createDocumentFragment();
for(;i&0;i--){
div= document.("div");
div.(document.createTextNode(i+ " - "+ newDate().toTimeString()));
frag.(div);
document.getElementById("nodes").(frag);
functiongrow(){
x.push(newArray(1000000).join('x'));
createSomeNodes();
setTimeout(grow,1000);
当调用 grow 的时候,它会开始创建 div 节点并且把他们追加到 DOM 上。它将会分配一个大数组并将它追加到一个全局数组中。这将会导致内存的稳定增长,使用上面提到的工具可以观察到这一点。
垃圾收集语言通常表现出内存用量的抖动。如果代码在一个发生分配的循环中运行时,这是很常见的。我们将要寻找那些在内存分配之后周期性且不会回落的内存增长。
查看内存是否周期性增长
对于这个问题,timeline 视图最合适不过了。在 Chrome 中运行这个例子,打开开发者工具,定位到 timeline,选择内存并且点击记录按钮。然后去到那个页面点击按钮开始内存泄露。一段时间后停止记录,然后观察结果:
这个例子中每秒都会发生一次内存泄露。记录停止后,在 grow 函数中设置一个断点来防止 Chrome 强制关闭这个页面。
在图中有两个明显的标志表明我们正在泄漏内存。节点的图表(绿色的线)和 JS 堆内存(蓝色的线)。节点数稳定地增长并且从不减少。这是一个明显的警告标志。
JS 堆内存表现出稳定的内存用量增长。由于垃圾回收器的作用,这很难被发现。你能看到一个初始内存的增长的图线,紧接着有一个很大的回落,接着又有一段增长然后出现了一个峰值,接着又是一个回落。这个情况的关键是在于一个事实,即每次内存用量回落时候,堆内存总是比上一次回落后的内存占用量更多。也就是说,尽管垃圾收集器成功地回收了很多的内存,还是有一部分内存周期性的泄露了。
我们现在确定程序中有一个泄露,让我们一起找到它。
拍两张快照
为了找到这个内存泄漏,我们将使用 Chrome 开发者工具红的 profiles 选项卡。为了保证内存的使用在一个可控制的范围内,在做这一步之前刷新一下页面。我们将使用 Take Heap Snapshot 功能。
刷新页面,在页面加载结束后为堆内存捕获一个快照。我们将要使用这个快照作为我们的基准。然后再次点击按钮,等几秒,然后再拍一个快照。拍完照后,推荐的做法是在脚本中设置一个断点来停止它的运行,防止更多的内存泄露。
有两个方法来查看两个快照之间的内存分配情况,其中一种方法需要选择 Summary 然后在右面选取在快照1和快照2之间分配的对象,另一种方法,选择 Comparison 而不是 Summary。两种方法下,我们都将会看到一个列表,列表中展示了在两个快照之间分配的对象。
本例中,我们很容易就可以找到内存泄露:它们很明显。看一下(string)构造函数的 Size Delta。58个对象占用了8 MB 内存。这看起来很可疑:新的对象被创建,但是没有被释放导致了8 MB 的内存消耗。
如果我们打开(string)构造函数分配列表,我们会注意到在很多小内存分配中掺杂着的几个大量的内存分配。这些情况立即引起了我们的注意。如果我们选择它们当中的任意一个,我们将会在下面的 retainer 选项卡中得到一些有趣的结果。
我们发现我们选中的内存分配信息是一个数组的一部分。相应地,数组被变量 x 在全局 window 对象内部引用。这给我们指引了一条从我们的大对象到不会被回收的根节点(window)的完整的路径。我们也就找到了潜在的泄漏点以及它在哪里被引用。
到现在为止,一切都很不错。但是我们的例子太简单了:像例子中这样大的内存分配并不是很常见。幸运的是我们的例子中还存在着细小的 DOM 节点内存泄漏。使用上面的内存快照可以很容易地找到这些节点,但是在更大的站点中,事情变得复杂起来。最近,新的 Chrome 的版本中提供了一个附加的工具,这个工具十分适合我们的工作,这就是堆内存分配记录(Record Heap Allocations)功能
通过记录堆内存分配来发现内存泄露
取消掉你之前设置的断点让脚本继续运行,然后回到开发者工具的 Profiles 选项卡。现在点击 Record Heap Allocations。当工具运行时候你将注意到图表顶部的蓝色细线。这些代表着内存分配。我们的代码导致每秒钟都有一个大的内存分配发生。让它运行几秒然后让程序停止(不要忘记在此设置断点来防止 Chrome 吃掉过多的内存)。
在这张图中你能看到这个工具的杀手锏:选择时间线中的一片来观察在这段时间片中内存分配发生在什么地方。我们将时间片设置的尽量与蓝色线接近。只有三个构造函数在这个列表中显示出来:一个是与我们的大泄露有关的(string),一个是和 DOM 节点的内存分配相关的,另一个是 Text 构造函数(DOM 节点中的文本构造函数)。
从列表中选择一个 HTMLDivElement 构造函数然后选择一个内存分配堆栈。
啊哈!我们现在知道那些元素在什么地方被分配了(grow -> createSomeNodes)。如果我们集中精神观察图像中的每个蓝色线,还会注意到 HTMLDivElement 的构造函数被调用了很多次。如果我们回到快照 comparison 视图就不难发现这个构造函数分配了很多次内存但是没有从未释放它们。也就是说,它不断地分配内存空间,但却没有允许 GC 回收它们。种种迹象表明这是一个泄露,加上我们确切地知道这些对象被分配到了什么地方(createSomeNodes 函数)。现在应该去研究代码,并修复这个泄漏。
其他有用的特性
在堆内存分配结果视图中我们可以使用比 Summary 更好的 Allocation 视图。
这个视图为我们呈现了一个函数的列表,同时也显示了与它们相关的内存分配情况。我们能立即看到 grow 和 createSomeNodes 凸显了出来。当选择 grow 我们看到了与它相关的对象构造函数被调用的情况。我们注意到了(string),HTMLDivElement 和 Text 而现在我们已经知道是对象的构造函数被泄露了。
这些工具的组合对找到泄漏有很大帮助。和它们一起工作。为你的生产环境站点做不同的分析(最好用没有最小化或混淆的代码)。看看你能不能找到那些比正常情况消耗更多内存的对象吧(提示:这些很难被找到)。
如果要使用 Allocation 视图,需要进入 Dev Tools -> Settings,选中“record heap allocation stack traces”。获取记录之前必须要这么做。
Memory Management C Mozilla Developer Network
J Memory Leaks C Douglas Crockford (old, in relation to Internet Explorer 6 leaks)
Java Memory Profiling C Chrome Developer Docs
Memory Diagnosis C Google Developers
An Interesting Kind of Java Memory Leak C Meteor blog
Grokking V8 closures
在垃圾回收语言中,如 Java,确实会发生内存泄露。一些情况下我们都不会意识到这些泄露,最终它们将会带来毁灭性的灾难。正是由于这个原因,使用内存分析工具来发现内存泄露是十分重要的。运行分析工具应该成为开发周期中的一部分,特别是对于中型或大型应用来讲。现在就开始这么做,尽可能地为你的用户提供最好的体验。动手吧!
译者简介 ( 点击 → 加入专栏作者)
ARIGATO:一个专注的iOS开发者
关注「前端大全」
看更多精选前端技术文章
推荐内容&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
苹果、华为搞起了移动AI芯片,但开发者们似乎意见很大
AlphaGo新版本问世:自学40天就能胜赢柯洁的版本
人工智能如何彻底变革人力资源格局,如何部署?
如果AI能够测试软件修复bug,程序员会更轻松吗
英国政府发人工智能深度报告,力图保持领先地位
语音和面部识别技术能帮助AI在情商上超越人类吗
啪又一张投资人名片入盒,梅卡曼德创始人邵天兰无奈地苦笑了下。 本来来参加工博会是为了吸引客户,却也吸引了超过五十个投资机构来学习一下,邵天兰对此颇有些心疼:我花了二十几万参展,每天大量客户来问经常都接待不过来,不请自来的投资人太多搞得我更是
本土协作机器人公司遨博智能在上海工博会上宣布获得复星集团A轮6000万人民币融资。 这是继两年前,遨博(北京)智能科技有限公司(遨博智能)获得盾安环境(00万天使轮融资后再一次融资。 遨博智能董事长魏洪兴接受澎湃新闻记者采访时表示,遨博智
谁会想到,自动驾驶汽车的故事竟然会成真,甚至机器学习算法能够带动计算机与人类交流、驾驶汽车、玩游戏,也可以做人类无法做到的事情。数学算法驱动的机器学习以及科学创新已经成为我们生活的重要组成部分。例如,谷歌应用概率算法自动纠正拼写错误的单词
人工智能技术在课堂中的应用,应该说是打开了一个全新的教育信息化应用领域,源于课堂一线的大数据让个性化的教与学有了科学的支撑。中庆智课自动运行,对授课教师和每一个学生的教与学行为进行数据采集和分析。
据物理学家组织网16日报道,英国萨塞克斯大学和斯旺西大学的科学家找到了一种办法,通过电场让液态金属变身为各种二维形状,如字母和心形等。研究人员表示,最新成果有望在智能电子设备、软体机器人以及柔性显示屏等领域大显身手。 用来改变液体金属形状的电
12,这绝对是一个令人沮丧的数字。在美国,仅有12%的工程师是女性。计算机行业中的情况要乐观一些,女性劳动力占该行业劳动力总数的26%。然而,相比于1990年的35%,事实上它仍然是下降了。 美国社会正在面临一个严峻的考验,即如何鼓励女性从事科学,技术,
日,美国麻省大学阿默斯特分校全球部在上海普华永道创新中心举办科技公开日活动,分享物联网世界对职业发展的影响。麻省大学阿默斯特分校(UmassAmherst)邀请了人工智能、物联网领域的专家,他们是最了解眼下工程学人才就业的情况的人。 作为一}

我要回帖

更多关于 内存泄漏造成死机 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信