一、内存泄漏
指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费(百度搜的资料)。这段话是什么意思呢?打个比方吧,放牛人与一只牛,放牛人好比程序设计中的引用,牛好比对象。我们通过引用来控制对象,放牛人通过绳子牵着牛,当牛吃完草后应该牵它回家而不是放手让它走失。当我们失去对该对象(某块内存)的控制时就产生了“内存泄露”。
二、垃圾回收
简单点说不用的内存都是垃圾,而垃圾回收就是把没用的内存都回收以释放内存空间。
AS3的垃圾回收主要有两种方法:“引用计数法”和“标识清除法”。
引用计数法:当一个内存对象添加了一个引用的时候,这个计数器就加1,当删除一个内存对象的引用时,该计数器就减1。当FP判断计数器为0的时候表示这个对象已经没有引用了,没有引用就没办法控制他了,符合垃圾回收机制的条件。但有一种情况则会有所不同!当有多个对象互相引用的时候,所有的计数器始终为1,这时就出现了内存泄露的情况。举个例子:
var a:Object = {}
var b:Object = {foo:a};
a.foo = b;
a=null;
b=null;
上述代码中,所有对象的引用都被删除了。没有任何办法在程序中再访问这两个对象了,但这两个对象的引用计数器都是1,因为它们相互引用。 即该两个内存块确实是没用了,但是垃圾回收机却不清理它。可想而知,这个办法在一般情况下还是不错的,但是遇到这种情况就不行了!于是要考虑第二种
方法了。
标识清除法: 这个方法在执行的时候会从FLASH的根对象也就是root开始到每一个引用的对象做标识。然后FLASH会遍历当前程序中的所有引用,发现没有标识的对象就清除。这样的话清除就准确了!但是会遇到一个问题。遍历是很消耗CPU的,虽然fp9通过调整迭代标识清除缩减对CPU的占用,但是消耗依然很大!所以采用了另外一种比较消极的处理方法!就是分段执行,每次执行一部分,然后偶尔执行一次。这样来减小CPU的消耗。所以我们通常知道,FP9、10的垃圾回收机是偶尔执行的!
三、垃圾回收的函数方法
System.disposeXML()
BitmapData.dispose()
Loader.unloadAndStop()
System.gc() ,这是一个最有用的函数,却只适用于调试版的FP
removeChild()
removeEventListener()
设置引用为null
System.totalMemory,返回值是内存占用字节数,这个跟内存回收没什么关系,只是用来查看内存占用,调试常用到
以上函数都是垃圾回收会用到的方法,不过一般需要按具体情况具体去确定综合运用几个方法。这个需要你真正理解FP的内存回收机制才会懂得如何运用。
AS3中的内存泄露与垃圾回收
今天有人在QQ群里问关于内存泄漏的问题,说实话,我从来没有正式的关注过这个问题,虽然在以前写程序的时候也为这个问题所困扰过,但是真的不了解其中的运行原理。内存泄露与垃圾回收可以说是程序在运行的时候遇到的两个比较重要的问题,这关系到我们程序是否能在机器中正确而有效的运行,你总不希望当你的程序运行一段时间后就弹出“内存******不能为read”的问题吧!很多计算机用户都特别讨厌这个提示。因为只要出现这个错误,程序就会立即关闭(大多数情况下是这样的!)令人烦恼的是,这种错误不可避免。因为一个应用程序少则几千行代码,多则几十万行,甚至上百万行。虽然内存泄露可以通过一定的技术手段排查出来,但是面对这么多的代码,相信再有经验的程序员都会望而却步。所以,我们在编写我们的程序时,尽量避开这些问题。像AS3这样的语言处理内存是贵垃圾回收机管的,我们不能自己去执行清楚内存操作,除非你用的是C或C++。其实现在大部分编程语言都用的是垃圾回收机,在一定程度上给我们的程序编写带来了便利,但同时也会产生问题,比如java,c#,vb等等,这些语言都是使用的垃圾回收机。AS3也是,那么垃圾回收机在什么情况下会产生内存泄露呢?
首先,你要明白什么是内存泄露。在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。(摘自百度百科)其实,内存泄露很简单,就是我们的程序创建了一个内存快,然后程序继续执行,最后,程序无法在控制这个内存块了。这就好比放羊,我们应该控制我们的羊群去吃草,而不是羊吃完草后,羊就丢了!看似是一个笑话,可这种情况在我们的程序中常常能够见到。知道了内存泄露,你也应该明白我们的程序有效的控制内存是多么的重要了吧!下面我们说说FLASH是如何控制内存的,以及什么时候FLASH对内存失去控制。
垃圾回收机!什么是垃圾?不用的内存不就是垃圾吗?正确!我认为没用的内存都是垃圾!(因为我的机器只有512M内存,内存对我来说太重要了!)清除内存迫在眉睫!但是FLASH不会像你想象的那样勤快的工作。假设我们创建了一个对象。
var a:Object = {name:mebiusshan};
好了!你能从中看到什么呢?难道你只看到了一个object对象吗?那你的目光就有些短浅了!最少你应该知道,在这个语句中我们创建了一个引用和一个内存块(我不知道内存块用来这里是否合适,暂且这么说吧)引用即a,这是对内存的引用,当有了这个引用之后我们就可以操作这个内存了!太帮了!打一个比方,就好像我们遛狗一样,我们需要一条绳子,套住我们的爱犬!然后你才能放心的去遛狗。好了!当我们创建这样一个对象的时候,FLASH会做什么呢?
先说第一个吧!它的名字叫做“引用计数法”,从名字我们就可以看出来,当一个内存对象添加了一个引用的时候,这个计数器就加1,当删除一个内存对象的引用时,该计数器就减1.当FLASH程序判断,计数器为0的时候,那么表示,这个对象已经么有引用了,没有引用就没办法控制他了,马上删除。这样就OK了!怎么样,这个设计不错吧!想法很好,思路简单。但有一种情况就麻烦了!注意,当有多个对象互相引用的时候,所有的计数器始终为1,这时就出现了内存泄露的情况。该内存确实没用了!但是呢,垃圾回收机却不清理它!郁闷,如果你的机器是512M内存的话。。。。。可想而知,这个办法在一般情况下还是不错的!但是遇到这种情况就OVER了!~那还有第二种方法吗?YES!放心,我们还有第二手准备,它的名字叫“标识清除法”。
这个方法在执行的时候会从FLASH的跟对象也就是root开始到每一个引用的对象做标识。好了!这是第一步,然后FLASH会遍历当前程序中的所有引用,发现没有标识的对象就清除。这样的话清除就准确了!但是会遇到一个问题。遍历是很消耗CPU的,虽然fp9通过调整迭代标识清除缩减对CPU的占用,但是消耗依然很大!所以采用了另外一种比较消极的处理方法!就是分段执行,每次执行一部分,然后偶尔执行一次。这样来减小CPU的消耗。所以我们通常知道,FP9、10的垃圾回收机是偶尔执行的!
GC和内存泄露无关
垃圾回收,这次是一个被无数人讨论过的传统话题。
Action Script使用的是和Java相似的内存管理机制,并不会即时回收废弃对象的内存,而是在特定时间统一执行一次GC(Gabage Collection)操作来释放废弃对象的内存,避免了重复判断是否需要回收产生的性能问题。
但要注意,这只是决定回收的时机,而不是回收的内容。这个延迟执行内存回收也就是个表面的现象,不管什么时候执行GC,能够回收的内存最终都能回收,不能回收的肯定不能回收。唯一的影响是,因为回收是延迟执行的,你在查看内存的时候不能直观地看到因为一个对象被废弃而回收内存的过程,会产生迷惑。
但这对于解决内存泄露是无关紧要的。
内存泄露指的就是当你销毁了一个对象的时候,它占用的内存却无法被回收,这会导致可用内存越来越小最终溢出,在内存紧张的环境中将会造成系统崩溃。其原因多种多样,但一般都是开发者的疏忽所致,没有提供给系统足够的可以销毁对象的依据。
执行GC虽然和内存泄露没有关系,但是如果不在测试前执行GC,你将看不到当时实际的不可回收内存的量,而内存泄露就是指不可回收内存的数量的增加。因此,测试内存回收将离不开GC方法。没有使用GC方法的测试用例是没有意义的,因为这其中掺杂了偶然性(什么时候执行GC)。不少荒谬的测试结果都是因为没有在正确的位置执行GC导致的。
Flash Player虽然没有开放发布状态的手动gc,但调试版本是可以使用的,正好可以让我们测试。此外下面的HACK代码也可以在发布阶段触发GC。
try {
new LocalConnection ().connect ( "gc" );
new LocalConnection ().connect ( "gc" );
} catch ( e:Error ) {}
但我再次强调,调用GC仅仅是用于测试。实际产品中调用GC基本没有意义(除了用于控制GC时机),总之如果你的程序出现了内存泄露,那一定和GC没有关系,请不要再在这种地方浪费宝贵的时间与精力。
只有在申请内存时才会触发自动GC
AVM2的GC是在每次申请内存时,根据当前内存占用来触发的。申请内存是一个必要因素。所以,如果你一直不进行申请内存的操作,就算内存达到了一个高值,它也不会进行GC。
这确实是个不合理的地方。但是,在实际环境中,一直不请求内存的情况是很少见的,就算出现,当时也未必处于内存的高值。这种情况主要出现在测试环境中,导致一些人会怀疑自动GC的功能是否正常。实际上这也是没有必要的。
Flash中垃圾回收的条件
在AVM2中,除去特殊的BitmapData必须调用dispose才能回收内存外,其他的部分都是用引用计数法和标记清除法作为判断是否应该回收内存的手段,而且并没有提供主动回收的API.
因此,你要回收一个对象,只要保证没有任何对象引用它,而且他的方法没有被当做事件函数——或者说,他和程序的其他部分已经没有任何联系,它就满足了引用计数法的标准,就一定会被回收。做到这一点的方法就是一般说的“执行removeChild,removeEventListener,将对他的引用设置为null”。
但是,实际上回收一个对象的要求并没有那样严格,就在于FP除了引用计数法,还包括标记清除法。标记清除法是从程序的根对象开始(stage,静态属性,活动的定时器和加载器,ExternalInface.callBack)一级一级遍历对象,只要遍历不到,即使不满足引用计数法的条件也可以回收。比如两个对象互相引用,但是和外界都没有关系,形成了孤岛,它们就可以被回收,尽管它们因为互相引用使得引用数不为0。比起单纯的引用计数,这种办法能确实能找到已经无法再访问到的实际上的闲置对象。所以,可以看到很多人的代码实际上并没有设置null,甚至没有removeEventListener,它一样可以被正常回收,少写这些代码可以使得程序更简洁,要全部符合标记清除法的条件,会很累。
“无法被根访问”,这种说法很暧昧,基本不能当做判断依据。所以我下面会举几个具体例子,来说明什么样的情况是符合标记清除法的要求的。
首先明确一点,标记清除法是只以能否能被根访问作为唯一依据的,并不需要关注被引用的次数,请不要混淆。
· 属性的相互引用是很明确的,一般都是一个对象包含着若干属性,那么这个对象自然可以维持它的属性的引用。如果这个类不会被回收(能够被根访问),他的所有属性也都不会被回收。同样的,如果这个类可以被回收的话(不能被根访问),也就不会妨碍属性的回收。所以你并不需要将所有属性设置为null,除非你希望在对象存在时候就回收其属性的内存,这种需求基本不存在。
· 静态属性是一个特殊的情况。静态属性本身就是根,所以你必须将其设置null才有可能被回收,没有别的办法。
· 至于在显示列表中的对象。既然根(stage)可以用getChildAt访问到自己的所有子对象,那么只要你在显示列表中,就肯定不会被回收。然而,如果显示对象的父层对象已经不再显示列表内,它的子对象就算还在父层对象之中也没有关系,因为它已经不能被stage访问到了。所以你不需要removeChild各层的全部对象,而只需要removeChild最高一层的父对象即可。
· A.addEventListener(“event”,B.handler),像这样添加过事件后,你可以认为B.handler成为了A的一个属性(因为A在需要的时候要能调用B.handler),这里也符合属性相互引用的原则。但是事件判断起来的确要比属性麻烦,因为相互引用的情况很多。在这里可以分为三种情况:
·
1. 对自己监听自己的事件,这相当于用自己的属性保存自己引用,任何情况都不会阻碍自己被回收。
2. 对自己的子对象(属性或者child)监听自己的事件。因为子对象本来就是自己在维持它的引用,那么即使它们会维持你的引用,也只会形成一个循环。一旦你和stage脱离了联系,子对象同样也会脱离联系,当然也无法妨碍你自己被回收了。除非子对象因为一些原因可以单独维持引用(诸如被保存在静态属性中),但这种情况很少见。
3. 对自己的父对象(parent或者stage)监听自己的事件。因为这使得你成为了父对象的一个属性,只要parent或者stage不被回收,那么自己就不会被回收。尤其是stage,它肯定不会被回收。这种情况一般都会导致自己无法回收,是必须removeEventListener的。
总得来说,就是务必注意对stage,parent的事件监听,其他情况一般都是不会妨碍回收的。而对stage,parent的监听大多都是各种鼠标,键盘事件。数量并不多,专门注意这里可以杜绝大部分因为事件造成的内存泄露。
其实,内存泄露并不容易出现。按照普通的编程习惯,只有监听stage事件这种做法会造成意料之外的泄露,一般都是可以顺利回收的。这比每次都要手工回收内存要方便多了。
这里只有BitmapData是例外。除了遵从上面的规则外,要回收它的内存,必须手动调用dispose方法,习惯自动回收的人会很累。务必注意,Bitmap对象的bitmapData属性是需要手动销毁的,Loader加载的位图是需要手动销毁的,当你用一个生成的位图作为位图填充绘制平铺的图像后,在销毁这个图像后也必须销毁这个位图(所以你必须一直保存位图的引用)。BitmapData是32位的未经任何压缩的图像,随便一个体积都会非常大,不处理好它们的回收,一个BitmapData泄露就可以顶你数万个复杂对象的泄露。
如果出现非常明显的内存泄露,大部分时候都是位图泄露。所以在研究上面的引用计数法和标记清除法以及GC之前,请先保证位图部分不出问题。
弱引用时的例外
弱引用会改变垃圾回收的规则。如果使用了弱引用,addEventListener将不会影响对象回收,即使对stage添加监听,也不会导致自己被回收。但是这同时也是缺点,因为有的时候你就是希望用引用限制住对象的回收,使用弱引用会使得这个对象有时回收有时不回收。虽然极少出现,但一旦出现,这种不容易重现的错误是很难查出来的。因此我并不推荐使用弱引用。
弱引用在AVM2中只有两处:
· 一处是addEventListener的第5个属性,名为userWeakReference,设置为true,监听事件将不会影响对象回收。
一处是Dictionary的构造函数参数,名为weakKeys,设置为true,当键为复杂对象时,即使Dictionary存在,键依然可以被回收。注意,这里说的是键,不是值,值是不享受弱引用待遇的。这个属性也写得也很明白,是weakKeys。
内存泄露的查找方法
Flash Builder提供了一个概要分析工具,可以帮助我们查找内存泄露。大多数情况都可以帮助我们解决问题。
关键点在于,检测内存泄漏应该是“创建,取样,销毁,再创建,取样”,然后以两次取样的对比数据来观察泄露。因为对象在第一次创建时会有一些缓存数据,它们在设计上就不会随着对象销毁而回收的,比如类定义的缓存,比如皮肤。它们只会创建一次,和我们看到的泄露并不是一回事。
必要时可以执行强制GC
因为每次GC都需要消耗性能,对象越多,GC越慢。我理解Flash Player禁用发布版本的System.gc()是为了避免开发者滥用这个方法,但有些时候我们的确需要手动控制GC时机,因为GC过程如果遇到大量可回收对象会让Flash Player卡住。
比如,我们需要在切换屏幕时回收一次内存,这时候卡是看不出来的,而不是切换完后播放动画时回收然后让动画顿住。或者,我们会定期在必要的时候执行一次GC,将GC需要的时间分担开。所以这时候用HACK方法强制执行一次GC也不失为一个选择。当然,这和内存泄露半点关系都没有。
Flash Player这个地方的设计特别的不好。它自己又不支持分步GC,一旦GC的时候没有办法避免卡的问题。结果GC的时机还不给控制……
微量剩余内存
测试中FLASH的确存在微量内存无限增加的问题,原因未知。我将50万个对象扔在一个数组中,销毁后确实会多出1M的内存占用(如果没扔在数组中不会),但这个数量很小,但达到能看得出来的100M内存需要5000万个对象,这个数额在通常情况下很难达到。
不过也有人说这只是对象销毁而内存并未全部释放的表现,实际上最后还是能完全释放的。或者是因为totalMemory的不精确所造成的。这个我就不清楚了。
不过就算这个的确是FlashPlayer的BUG,也无伤大雅吧
AS3.0中对侦听器的改进远远不止以上这些,看一看addEventListener的实现接口:
[java] view plaincopyprint?
- function addEventListener(eventName:String,
- listener:Object,
- useCapture:Boolean=false,
- priority:Integer=0,
- useWeakReference:Boolean=false):Boolean;
function addEventListener(eventName:String,
listener:Object,
useCapture:Boolean=false,
priority:Integer=0,
useWeakReference:Boolean=false):Boolean;
哇塞,有三个莫名奇妙的参数。可是当你知道这三个参数背后隐藏的巨大改进之后,相信你要大叫三个哇塞。
第一个神秘参数,目前暂不解释,埋个伏笔,留到EventDispatcher那一节,讲ActionScript3崭新的Event Flow事件流机制.
第二个参数:优先级。
有趣吧,在ActionScript3.0中我们可以控制事件的优先级,从而达到控制function侦听器的执行顺序。如果你不填这个参数,那么事件默认为同一个级别0,事件的执行按先来后到的天经地义的顺序。
如果设为1,那么事件级别降一个档次,稍后执行。数字越高优先级越高。级别可以为负数。
(注意: Flex Builder 2 Beta3中事件级别是越低越高)
第三个参数,讲的是是否设为弱引用。
兄弟们初学As2.0时一定经常忘了在删除Listener对象时,却忘了removeEventListener吧。这会导致很多莫名奇妙的情况发生。也是最常见的诡异bug种类之一。即使老鸟偶尔也会被阴一把。
最郁闷的是这种情况往往不被发现,在后台默默的消耗大量资源。
现在可以用弱引用可以在某种程度上解决这种bug。
设为true,就是告诉垃圾回收器,这个侦听器function的引用是弱引用。一旦这个侦听器在运行时只剩下了这一个弱引用,那么垃圾回收器可以不理它,直接把它回收咯屁了,节省资源。
从AS3.0引入这个弱引用这个概念就可以看出,AS3.0是如何的重视资源管理和有效率的运用。一个标准的重量级的程序语言必须具有这样的特征。
=================
弱引用的使用原则
弱引用从原则上来讲,其引入是为了防止无意识的对象保留(unintentional object retention)引起的内存泄漏。
什么是无意识的对象保留?一般情况下,对象的逻辑生命周期和实际生命周期应当相同。但是在某些情况下,比如事件侦听器机制,我们可能会创建一个引用,它在内存中生存的时间比我们预期的要长很多。比如说,下面的例子中,即使删掉了侦听器lis,但事件还是能被继续捕捉。这是由于并没有removeEventListenr,那么系统中还会保留着对侦听器的引用。
如果没有把addEventListener中对侦听器的持有改成弱引用,那么这个侦听器会一直存在、耗掉内存不被人发觉。但如果设成了弱引用,那么垃圾回收器过段时间后会将侦听器占用的内存回收,此时侦听器从物理意义上才真正的被销毁了。
在销毁之前,这个侦听器还会继续存在,继续作用。这就是为什么下例中虽然已经把useWeakReference设为了true,但是还是会有作用。
那么什么时侯垃圾回收器(Garbage Collector)才会来回收呢?
唔,这要看它高兴了,快的话10分钟,慢的话一个月。哈哈,开玩笑,垃圾回收器的工作时间咋一看确实是不可测的,但也有规律可循。
那么推论来了,我们凭什么要把useWeakReference设为false呢?除非我们有意要让某个侦听器在失去了其他所有引用的时候还要工作,才有可能这样做。但谁会有这样疯狂的想法呢?道哥还是包世宏?
所以,我建议,大家不妨养成习惯,把useWeakReference设为true。
当然最好的做法是始终记得不用侦听器了,一定要removeEventListener。在这一点上,我们要坚持“始乱终弃”!!
[java] view plaincopyprint?
- //这个是Document Class,建个fla,绑定这个class。忘了,就看我第三篇教程。
- package
- {
- import flash.display.MovieClip;
- import flash.events.EventDispatcher;
- import flash.events.MouseEvent;
- public class LearnEvents extends MovieClip
- {
- function LearnEvents() {
- var lis:Function = function () {
- trace ("听到了鼠标Click事件!");
- }
- var kingda_s:Kingdasprite = new KingdaSprite(0xFFCC00, "kingdaSquare");
- addChild(kingda_s);
- kingda_s.addEventListener(MouseEvent.CLICK, lis, false, 0, true); //瞧,最后一个参数,已经把弱引用设为true了
- lis = null; //这一句,我已经从逻辑上删掉lis了,大家作证啊
- trace ("侦听器还在吗?:"+ lis); //lis也确实指向null了。但你只要继续点击方块,你会发现Click事件仍然被捕捉到。
- }
- }
- }
- import flash.display.Sprite;
- import flash.events.MouseEvent;
- //这个类就是画一个矩形,
- class KingdaSprite extends Sprite {
- public var nickname:String;
- public var ColorNum:uint;
- //colorNumber就是#ffcc00这种类型的数,在AS3中推荐用新的uint型来标记它
- public function KingdaSprite(colorNumber:uint, nameString:String) {
- ColorNum = colorNumber;
- nickname = nameString;
- graphics.beginFill(ColorNum);
- graphics.drawRect(0,0,100,100);
- graphics.endFill();
- }
- }