谈谈ActionScript垃圾回收(下)

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

前文我们介绍了GC的工作机制和帮助GC更好工作的最佳实践。其实只要我们遵守谁创建谁清理的原则来管理对象,就能基本上避免回收失败,也就是我们通常说的内存泄漏问题。但是在实际项目中我们还会看到各种原因引起的内存泄漏,接下来就让我们一起来找出病因。

首先我们需要观察症状,也就是内存的使用曲线。排查的方法是反复执行一些创建和删除对象的方法、反复加载和卸载子文件。如果内存曲线一路飙升、或者是居高不下,都表明发生了内存泄漏问题。观察内存占用可以直接求助于操作系统的资源管理器,也可以用Hi-ReS-Stats这个类。

第二个需要观察的地方,是Player输出的load和unload信息。加载和卸载外部文件,是内存泄漏问题的重灾区。在调试阶段,我一般会在主文件加一个执行System.gc()语句的按钮。一旦卸载了一个子文件,就手动触发若干次GC。如果没有输出子文件的卸载信息,那么就说明出现泄漏了。

第三个可以帮助我们排查问题的地方是Profiler工具,当你删除了对象引用,并手动触发GC以后,可以观察这个对象是否还存在内存中。Profiler可以说是排查内存泄漏问题的终极工具,唯一的问题就是会拖慢整体的运行速度,比较慢。

观察到问题现象以后我们得顺藤摸瓜,找出到底是那个对象占着内存不放,然后对症下药。下面我们就来分析几个内存泄漏的疑难杂症。

病例一:小心loaderContext和applicationDomain

ActionScript 3的Loader对象远没有我们想象中那么简单,内存泄漏问题有很大一部分是由于不当的加载和卸载操作引起的。我在研究Gaia框架的内存泄漏问题的时候发现了一处由于没有删除LoaderContext的引用而造成的卸载失败问题,其实就是没有释放应用程序域所造成的。应用程序域是一个需要被重视的对象,它对加载和卸载的影响有如下两点:

  • 如果子SWF文件是加载到主应用程序域里的,那么这个文件是不能卸载的(前提是子SWF文件内的类定义没有被主应用程序域里定义所覆盖)。
  • 如果子SWF文件是加载到子应用程序域内(Loader的默认方式),那么这个文件是一定能够被卸载的。

关于应用程序域的知识可以看我以前翻译的文章。根据类定义在主应用程序域里的向下覆盖原则,我们还可以考虑以下情况:如果再次加载相同的子SWF文件到主应用程序域,子文件里所包含的类定义将全部忽略,并不会注册到主应用程序域中。这次加载的SWF文件则是可以被卸载的。换句话说,一旦类定义被加入到主应用程序域里就不能够被删除。而没有加载到主应用程序域内的对象如果不能卸载就肯定是内存泄漏。

实际开发中除了一些确实不需要卸载的模块代码需要加载到主应用程序域中,一般我们还是将对象加载到子应用程序域中去的。

病例二:小心静态类

症状还是某个子文件加载后不能卸载,但是当我们再次加载这个子文件的时候,能从log看到之前的子文件被释放了。这是一个轻度内存泄漏的例子,一般不会引起内存飙升直到引起crash等强烈后果,但是我们也不能掉以轻心。

根据之前的经验:不能卸载一定是某个对象被占住了,后续再次加载又能卸载之前的实例,说明前面文件中被占住的资源又被释放了。我们先通过Profiler查看到底是那个对象被占住,然而分析下来看到居然是子文件中创建的所有实例都已经释放了。那么,到底是什么原因呢?

既然实例都已经被释放了,那么只有可能是类定义被占住了。我在这个子文件中用到了Greensock类库的ImageLoader。通过研究它的源码发现这个加载类库采用了与TweenMax类似的插件机制。当我第一次引用ImageLoader定义的时候,它会自动向LoaderMax类注册。也就是说LoaderMax类的静态成员持有ImageLoader定义的引用。

如果这两个类定义都在子应用程序域中,那么随着子文件的卸载,这两个静态类也会被销毁了。但是我在主文件中也包含了LoaderMax类,这个定义会覆盖掉我在子文件中的定义。于是造成的情况就是:一个主应用程序域中的LoaderMax类持有子应用程序域中的ImageLoader类的引用。这就是子文件无法卸载的原因!

解决方法很简单:要么在主文件中也包含ImageLoader类的定义,要么在主文件中删去LoaderMax类。这样我们就解决了一个由于跨域的静态类引用造成的内存泄漏问题。

从这个例子我们还可以总结一下在ActionScript中静态类、静态变量及其衍生的单例的注意事项,这也是和其他编程语言不同的地方:

  • 只要静态类的定义是在子应用程序域里的,那么是可以被卸载的。
  • 静态类、单例的只能保证在同一个应用程序域里的唯一性。也就是说有可能单例不单。
  • 真正保证静态类和单例的唯一性的方法是把它们的定义加入到主应用程序域。

这种静态类之间引用的问题也是唯一让Profiler束手无策的情况,如果以后能在Profiler中直接看到类定义来自哪个应用程序域就更好了。

除此之外还要小心的是静态类的方法可能造成的对象引用问题,比如:Flash组件的FocusManager.setFocus(),以及Flex框架中的StyleManager的样式注册等等。这篇文章详细讨论了Flex模块的卸载问题。

病例三:延时删除

这个无法卸载的问题来自于我的一个使用Robotlegs模块插件开发的子文件。为了让所有mediator执行自己的onRemove()方法,我在ShutdownCommand中将所有视图从contextView上移除,此外还进行了model和service自己的清理工作。这通常运行良好,能够正确的将模块卸载。但是我却遇到了一个问题,严格来说,这并不是一个GC的问题。因为我通过trace发现mediator的onRemove()方法并没有执行!

没有执行清理当然就有可能造成内存泄漏,那么到底是什么原因,让我从contextView上移除视图的时候没有触发对应mediator的onRemove()方法呢?

答案是Robotlegs的延时机制。为了兼容Flex框架,mediator的onRemove方法并不是在视图的REMOVED_FROM_STAGE事件监听里执行的,而是延迟了一帧(查看代码)。这样在真正的移除代码执行以前我的视图就已经从stage上移除了,也就过不了330行那个检查。

于是我就只好迁就一下Robotlegs,把子文件从显示列表上移除的时间也延迟了一帧,这样问题就解决了。

从这几个例子我们可以看出,内存泄漏的病因可能千奇百怪,但归根结底肯定都是某种引用没有被释放的问题。在实际项目中,建议大家一边开发一边就要测试内存泄漏。不要到了项目的最后阶段再来排查,那样复杂度太高。此外,在引入第三方类库的时候,也要特别注意是否会引起内存泄漏。

本文总结了排查内存泄漏的方法,分析了若干可能引起内存泄漏的代码问题。希望对大家有所帮助。如果同学们在自己的项目中也遇到过一些疑难杂症,欢迎留言一起探讨。

网上相关主题文章:

相关 [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会动态调整堆大小以满足需求,如果分配的空间大了,就会收缩堆,回收内存.