谈谈ActionScript垃圾回收(上)

标签: Develop ActionScript garbage-collection gc | 发表时间:2011-08-07 20:24 | 作者:Kevin Jia
出处:http://kevincao.com

在《给AS程序员的一点建议一文》中我提到了释放资源的重要性。最近在一些项目过程中我又对这方面有了更多的理解,在此希望能够分享给大家。首先让我们来回顾一下关于垃圾回收(Garbage Collection,下文简称GC)的一些知识。要阅读本文,你需要对GC机制有些基本认识。

在ActionScript中,我们没有API可以直接删除一个对象,也不能控制Player进行GC。但是GC的行为是可以预估的,作为开发者,我们需要了解的是GC执行的时机是发生在需要向操作系统请求分配内存的时候。

从上面的模拟图我们可以看到:

  • Player以块的方式请求和释放内存。GC的结果不一定就是更少的内存占用,也有可能是从操作系统获得更多的可用内存。
  • Player会在某些GC过程中把内存中未使用部分组合成可以释放的块还给操作系统。
  • 此外还要注意的是Player为了避免占用太多的CPU资源,会将一些GC操作分到不同的时间片中运行,所以一次GC过程并不一定清理完所有可回收资源。

一次GC过程(GC Pass)分为以下两个步骤:

Reference Counting

统计所有对象的引用计数,如果某个对象没有任何引用,就标记为可回收。

这个操作很好理解,需要强调的是weak reference(弱引用)是不参与计算的。引用计数是一个相对省CPU的操作,能够筛选出大部分可回收资源,但是对一些循环引用的情况就无能为力了。在下图中,标记为绿色的对象每个的引用数都为1,但它们明显是应该被回收的。

所以GC需要进行第二个步骤:

Mark Sweeping

这个步骤是从根对象(Root)开始轮询对象的引用。所谓的根对象包括:

  • Stage对象
  • 静态变量
  • 局部变量

这种方式足够精确,能够成功筛选出上图中绿色标记的对象,而它的代价就是较大的计算开销。

为了帮助GC过程更高效的执行,最好是能在第一步引用计数中就把需要回收的对象都标记出来。具体的做法就是把所有不需要的对象引用全部清空,包括:

  • 删除成员变量的引用
  • 从可视对象列表上移除对象
  • 移除事件监听

难点:事件监听是否会造成对象不能回收?这个问题要具体分析,有些情况可以,有些情况却不可以。归根结底还是引用关系的问题。来看下面这个例子:

Foo.as:
 
public class Foo extends Sprite
{
	private var bar:Sprite;
	public function Foo()
	{
		bar = new Sprite();
		//...
 
		//监听bar发出的事件。可以看作是bar引用了foo,因为foo的引用被保存在bar的监听者数组里。
		bar.addEventListener(MouseEvent.CLICK, clickHandler);
		addChild(bar);
	}
 
	private function clickHandler(event:MouseEvent):void
	{
		...
	}
}
Main.as:
 
// 创建foo实例
var foo:Foo = new Foo();
addChild(foo);
 
// do something with foo...
 
// 清除foo的引用
removeChild(foo);
foo = null;

我们看到foo引用了bar,而bar又通过事件监听的联系引用了foo,这就构成了一个循环引用。根据前文对GC步骤的分析,这两个对象都必须到第二步mark and sweep才能被标记出来。

如果我们对事件监听用弱引用的方式:

bar.addEventListener(MouseEvent.CLICK, clickHandler, false, 0, true);

由于弱引用不计入引用计数,所以现在foo的引用数为0。GC在第一步操作中就能把foo标记出来,从而减少了一些运算开销。这也是为什么Grant Skinner呼吁把弱引用作为事件监听的默认方式的原因。

但是作为最佳实践,我们还是提倡要手动移除事件监听。以下代码添加一个destroy()方法(也有习惯命名为dispose()或kill()等)到Foo对象中:

public function destroy():void
{
	bar.removeEventListener(MouseEvent.CLICK, clickHandler);
	removeChild(bar);
	bar = null;
}

在清除foo的引用之前执行destroy()方法:

foo.destroy();
removeChild(foo);
foo = null;

经过我们如此处理,两个对象的引用数全都为0。GC只需第一步处理就完成任务,我们节约了更多的运算开销!

从上例可以看出在一般情况下,监听子对象的事件不会影响GC。不管是弱引用方式的监听,还是显式移除监听,都只是帮助GC更高效运行的手段,而不会影响GC的结果。但是如果事件监听造成的是对象以外的引用关系,情况就不同了,并且很有可能造成回收失败。一个常见的错误例子是监听Stage对象的RESIZE事件,如果没有显式移除这个监听或者是没有采用弱引用方式,那么这个对象就不会被GC回收的。所以我建议大家还是要尽可能的显式移除监听,切断引用关系。

我们现在已经用对GC最友好的方式做好了清理准备,但是对象还没有从内存中删除。在等待GC执行的这段时间,对象内的代码还在继续执行。比如加在对象上的ENTER_FRAME事件监听处理还在继续执行,对象内的MovieClip或Sound都还在继续播放。我们一定要避免这种情况的发生,所以在切断引用之前,我们还要在destroy()方法中做些清理工作。我们要做的工作包括:

  • clearInterval(),clearTimeout()
  • timer.stop()
  • loader.unload()/loader.unloadAndStop()
  • movieclip.stop() 如果有子MC的,也要停止播放
  • bitmapData.dispose()
  • 关闭LocalConnection,NetConnection,NetStream
  • 停止音频和视频的播放
  • 删除Camera和Microphone对象的引用
  • 调用子对象的destroy()方法,如果有的话

其实这些都是在开发中管理资源的基本常识,归结为一句话就是:谁创建了对象,谁就要负责清理该对象。

下面就以一些我在实际项目中开发的destroy()方法为例,看代码说话:

public function destroy():void
{
	// List是一个继承自Sprite的自定义子类
 
	// 移除list的事件监听
	list.removeEventListener.remove(MouseEvent.CLICK, clickHandler);
 
	// 我们创建了list实例,也要负责清理
	// 调用list对象自己的destroy()方法
	list.destroy();
 
	// 将list从显示列表移除
	// 这一步并非必须的步骤,原因是list的父对象会被移除,这样list和stage就没有联系了。
	// 但是在我的代码中,list还有可能会被添加到外部的容器中(比如stage),那么这一步就是必须的了。
	list.parent.removeChild(list);
	list = null;
 
	// --------------------
	// loader是一个来自tweenmax类库中的ImageLoader实例
 
	// 如果loader已经创建
	if(loader)
	{
		// 调用loader的dispose()方法,优秀的第三方类库都应该有良好的资源管理机制。
		// 参数true表示把加载的内容从显示列表上移除,帮我们节约了代码。
		loader.dispose(true);
		loader = null;
	}
 
	// --------------------
	// lightbox实例变量保存了一个外部引用
	// 根据谁创建谁清理的原则,我们在这里不需要负责该对象的清理,只要删除引用就可以了。
	lightbox = null;
}

另一个示例的destroy()方法演示了对数组中对象的处理方法:

public function destroy():void
{
	// cells是一个数组,包含了一组子对象
	var i : int, n : int;
	n = cells.length;
	for(i = 0; i < n; i++)
	{
		// 执行每一个子对象的destroy()方法
		cells[i].destroy();
		// 删除子对象的引用
		cells[i] = null;
	}
 
	// 删除数组自身的引用
	cells = null;
 
	// 如果不需要得到每一个子对象的引用,我们也可以简单的用以下代码来清理数组:
	//cells.length = 0;
	//cells = null;
}

在结构更复杂的项目里,我们还可以抽象出一个IDestroyable接口,让需要执行清理的自定义对象实现这个接口。这样我们的清理代码可以写为:

var n:int = this.numChildren;
for(var i:int = n-1; i >= 0; i--)
{
	if(this.getChildAt(i) is IDestroyable)
	{
		IDestroyable(this.getChildAt(i)).destroy();
		this.removeChildAt(i);
	}
}

总结:GC好比是ActionScript城市的环卫工人,我们的每个类都是从事劳动生产的市民。优秀的市民会把生产垃圾分类安放到回收点,而不文明的市民则把垃圾丢得到处都是。你说那种做法让城市的清扫工作变得更加高效?所以请大家谨记“谁创建谁清理”的原则,做一位ActionScript好市民。

本文回顾了GC的一些基本知识和最佳实践。在下一篇中我将结合一些具体问题,为大家把脉GC的疑难杂症。敬请期待。

继续阅读:

谈谈ActionScript垃圾回收(下)

参考资料:

相关 [actionscript 垃圾回收] 推荐:

谈谈ActionScript垃圾回收(下)

- Tomyail - Kevin Cao's Blog
前文我们介绍了GC的工作机制和帮助GC更好工作的最佳实践. 其实只要我们遵守谁创建谁清理的原则来管理对象,就能基本上避免回收失败,也就是我们通常说的内存泄漏问题. 但是在实际项目中我们还会看到各种原因引起的内存泄漏,接下来就让我们一起来找出病因. 首先我们需要观察症状,也就是内存的使用曲线. 排查的方法是反复执行一些创建和删除对象的方法、反复加载和卸载子文件.

谈谈ActionScript垃圾回收(上)

- Jia - Kevin Cao's Blog
在《给AS程序员的一点建议一文》中我提到了释放资源的重要性. 最近在一些项目过程中我又对这方面有了更多的理解,在此希望能够分享给大家. 首先让我们来回顾一下关于垃圾回收(Garbage Collection,下文简称GC)的一些知识. 要阅读本文,你需要对GC机制有些基本认识. 在ActionScript中,我们没有API可以直接删除一个对象,也不能控制Player进行GC.

jvm垃圾回收

- Cano - 淘宝共享数据平台 tbdata.org
在jvm中堆空间划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和永久代(Permanent Generation). 年轻代和年老代是存储动态产生的对象. 永久带主要是存储的是java的类信息,包括解析得到的方法、属性、字段等等. 我们这里讨论的垃圾回收主要是针对年轻代和年老代.

JVM 垃圾回收算法

- - 码蜂笔记
《深入理解Java虚拟机:JVM高级特性与最佳实践》-笔记. 垃圾回收,Garbage Collection,简称GC. 判断对象是否存活一般有两种方式:. 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收. 此方法简单,无法解决对象相互循环引用的问题.

Java中的垃圾回收

- - Java译站
前文中对标记删除算法的介绍更多还是偏理论性质的. 实践中,为了更好地满足现实的场景及需求,还需要对算法进行大量的调整. 举个简单的例子,我们来看下JVM需要记录哪些信息才能让我们得以安全地分配对象空间. 碎片及整理(Fragmenting and Compacting). JVM在清除不可达对象之后,还得确保它们所在的空间是可以进行复用的.

Java垃圾回收调优

- - 编程语言 - ITeye博客
在Java中,通常通讯类型的服务器对GC(Garbage Collection)比较敏感. 通常通讯服务器每秒需要处理大量进出的数据包,需要解析,分解成不同的业务逻辑对象并做相关的业务处理,这样会导致大量的临时对象被创建和回收. 同时服务器如果需要同时保存用户状态的话,又会产生很多永久的对象,比如用户session.

JVM垃圾回收(GC)原理

- kill - yiihsia[互联网后端技术]_yiihsia[互联网后端技术]
引用计数(Reference Counting). 原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数. 垃圾回收时,只用收集计数为0的对象. 此算法最致命的是无法处理循环引用的问题. 标记-清除(Mark-Sweep). 第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除.

HotSpot 垃圾回收算法实现

- - 码蜂笔记
《深入理解Java虚拟机:JVM高级特性与最佳实践》-笔记. 在可达性分析期间整个系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况. 一致性要求导致GC进行时必须停顿所有Java执行线程. 即使在号称不会发生停顿的CMS收集器中,枚举根节点时也是必须停顿的. HotSpot使用的是准确式GC,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,这是通过一组称为OopMap的数据结构来达到的.

Erlang进程堆垃圾回收机制

- - CSDN博客推荐文章
原文: Erlang进程堆垃圾回收机制. 作者:http://blog.csdn.net/mycwq. 每个Erlang进程创建之后都会有自己的PCB,栈,私有堆. erlang不知道他创建的进程会用到哪种场合下,所以一开始分配的内存比较小. 如果分配的空间不够了,erlang gc会动态调整堆大小以满足需求,如果分配的空间大了,就会收缩堆,回收内存.