智慧 + 毅力 = 无所不能

正确性、健壮性、可靠性、效率、易用性、可读性、可复用性、兼容性、可移植性...

导航

Flash Player 和 Adobe AIR 垃圾收集内部机制

Posted on 2013-01-22 17:35  Bill Yuan  阅读(478)  评论(0编辑  收藏  举报

转自:http://www.adobe.com/cn/devnet/actionscript/learning/as3-fundamentals/garbage-collection.html

所有应用程序都需要管理内存。应用程序的内存管理包括很多方面的指导原则,比如何时分配内存、分配多少内存、何时将内容移至垃圾桶,以及何时清空垃圾桶。MMgc 是一种通用内存管理器,Flash Player 用它来完成几乎所有内存分配任务。了解 MMgc 管理内存的方式是优化代码和应用程序性能的重要环节。

由垃圾收集器自动回收的内存被视作“托管内存”。垃圾收集器决定内存何时不再被应用程序使用以及何时对其进行回收再利用。本文探讨内存分配、垃圾收集流程以及 Flash Player 11 和 AIR 3 中的全新pauseForGCIfCollectionImminent() API。

内存分配

Flash Player 利用页面分配器 (GCHeap) 分配操作系统中的大型内存块(兆字节)。随后,GCHeap 将大型数据块分解成较小的 4K 页面,并根据需要将这些页面分配给垃圾收集 (GC) 内存管理器。

图 1. GCHeap 分配操作系统内存,将其分解为 4K 页面,并分配给 GC。

随后,GC 使用这些 4K 页面为系统中的对象(大小最大为 2K)提供内存。

图 2. GC 会为小于 2K 的对象分配 4K 页面。

对于大于 2K 的对象(位图、视频和文件等),GCHeap 会为大型内存分配器分配多组连续的 4K 块。

当大型区块中的几乎所有 4K 页面均已分配完毕后,Flash Player 将运行垃圾收集,以便在 GCHeap 尝试分配更多操作系统内存前回收未使用的内存。换句话说,垃圾收集只能由内存分配触发。测试和配置期间尤其要记住这一点,因为这意味着闲置应用程序的内存使用将永不改变。

堆和堆栈

堆是指分配给运行期间创建或初始化的任何对象的内存。堆上的对象将持续存在,直到被当做垃圾进行收集处理。

图 3. 对象 A 位于堆上,被堆栈上的局部变量 o 所引用。

堆栈是用于存储编译期间定义的所有变量的内存。堆栈内存以连续的方式被使用和重用。Push 可向堆栈顶部添加元素。Pop 则可从堆栈顶部删除元素。访问堆栈中部元素的唯一方法是删除该元素上方的所有元素。

系统会在运行这些方法时将本地方法变量、参数及有关方法完成后返回何处的信息推送到堆栈。堆栈变化非常迅速。堆栈的对象引用也往往非常短暂。这些对象引用可能位于堆栈,但分配给这些对象的内存则来自堆。

图 4. 局部变量在定义之时即会推送到堆栈。有关方法完成后返回何处的信息也将被推送到堆栈。

Flash Runtime 垃圾收集实现方法

Flash Player 和 AIR 结合使用了延迟型引用计数和保守型标记再清除方法。

延迟型引用计数

在延迟型引用计数中,堆和堆栈引用存在重大区别。由于堆栈变化迅速且包含的引用也往往非常短暂,因此堆栈引用上不进行引用计数。引用计数只针对堆上的引用。

图 5. 对象持续跟踪记录自身所拥有的引用数目。

堆上的每个对象都会跟踪记录指向自身的引用数目。每当为对象创建引用时,该对象的引用计数都会随之递增。而当您删除引用时,该对象的引用计数则会随之递减。如果该对象有零个引用计数(没有任何引用指向它),系统会将其添加到零计数表 (ZCT)。当 ZCT 填满时,系统将扫描堆栈,查找从该堆栈到 ZCT 上对象的所有引用。ZCT 上所有不具有堆栈引用的对象均将被删除。

延迟型引用计数面临的问题之一是循环引用。如果对象 A 和对象 B 互相引用,但系统中没有任何其他对象指向它们,则它们将永远不会具有零引用计数,因此将永远没法使用引用计数进行垃圾收集。此时须使用标记再清除方法。

图 6. 对象 A 和对象 B 互相引用,但不具备任何其他引用。

标记/清除

Flash Player 或 AIR 中运行的应用程序具有多个 GCRoot。您可以将 GCRoot 视为树干,将应用程序对象视为分枝。Stage 就是一个 GCRoot,加载程序都是 GCRoot,某些菜单也是 GCRoot。应用程序仍在使用的每个对象均可从该应用程序内的其中一个 GCRoot 到达。GCRoot 不会被当作垃圾进行收集。

应用程序中的每个对象都有一个“标记位”。垃圾收集标记阶段开始后,系统将清除所有这些标记。MMgc 会跟踪记录应用程序中的所有 GCRoot。垃圾收集器从这些根开始,跟踪每个对象并为其到达的每个对象设置标记位。从任何根均无法到达的任何对象也无法从应用程序的任何位置到达,即标记阶段不会设置其标记位。一旦收集器标记完其自身找到的所有对象,将会随即开始执行“清除”阶段。系统将销毁未设置标记位的所有对象并回收其内存。

图 7. 循环引用中的对象尚未标记。

图 7 显示,每个可从 GCRoot 到达的对象均具有自己的标记位组(蓝色)。陷入循环引用的两个对象(对象 A 和对象 B)无法从 GCRoot 中到达。系统将不会为它们设置标记位。因此,即使没有零引用计数,这两个对象也将被当作垃圾收集。

弱引用

Flash Player 还能承载针对某些类型的对象的所谓的“弱引用”。弱引用是指对垃圾收集器普通跟踪过程(该过程跟踪所有根以查找可到达的对象)不可见的引用。

您实例化某个新字典时,可以指出自己希望对字典键进行弱控制。

var d:Dictionary = new Dictionary( true );
d[ someObject ] = someValue;

您还可以在添加事件侦听器时将addEventListener() 函数的 useWeakReference 参数设置为 true。

obj.addEventListener( "type", handler, false, 0, true );

在这两种情况下,您将要求 Flash Player 保持两个对象间的引用关系,但属于弱引用关系。实际上,这意味着标记期间不跟踪这种特定的引用。

图 8. 标记期间不会跟踪弱引用。

在这种情况下,到达对象 B 的唯一路径是弱的。跟踪期间不会遍历到,因此对象 B 不会被标记并收集。但是,如果有另一条强路径到达对象 B,则对象 B 会被标记并保留。

图 9. 具有强引用的对象在跟踪期间将会被找到并标记。

您应当经常通过删除字典中的未使用条目及使用removeEventListener() 来清理未使用的引用。但是,有时候清理未使用的引用是不可行或不可能的。比如说在您不知情的情况下类遭到实例化和破坏时,即以这种方式使用条目呈现器。在这些情况下,保持对象弱引用可让 Flash Player 最终删除引用并回收内存。

保守型收集

MMgc 被视作一种保守型标记/清除收集器。MMgc 无法分辨内存中的某些值是对象指针(内存地址)还是数值。为防止意外收集这些值可能指向的对象,MMgc 假定每个值均可以作为指针。因此,一些不会被实际指向的对象将永远不会被收集,而将被视作内存泄露。虽然您希望最大限度地减少内存泄露以优化性能,但由保守型 GC 导致的临时泄露往往随机发生,不会随时间增长,并且对应用程序性能的影响也比开发人员造成的泄露要小得多。

增量收集

不幸的是,垃圾收集可能会随收集过程的完成而导致 Flash Player 定期暂停工作。此类暂停与该应用程序当时使用的内存量成正比。暂停时间可能稍长,在某些程序中可以察觉到。

标记阶段是垃圾收集过程中最耗时的一个环节。鉴于这一事实,标记过程一直使用工作队列和三色算法实现增量。该队列负责保持标记增量之间的标记状态。

表 1.三色算法

在标记阶段之初,所有 GCRoot 均被推送到队列中并变成灰色。
 
图 10. GCRoot 在被推送到工作队列时变成灰色。

随着标记过程的继续进行,已标记对象变成黑色,并从工作队列中删除。

图 11. 已标记对象呈黑色,不再位于工作队列中。

毫无疑问,该过程将一直持续到一个新对象(白色)被添加到黑色对象。当发生这种情况时,白色对象将永远不会设置自己的标记位,因为其 GCRoot 已经标记。由于没有设置标记位,所以这些对象将会在清除阶段被当作垃圾收集。

图 12. 新对象被添加到之前已标记的对象。

为防止发生这一问题,MMgc 内会采用写屏障强迫添加到黑色对象中的任何白色对象被立即添加到工作队列。

图 13. 添加到之前已标记对象的新对象被立即添加到工作队列。

通过采用工作队列和三色算法,可启动和停止标记阶段,从而防止出现不必要的长时间垃圾收集暂停。

临近性

标记阶段可能是垃圾收集过程中最耗时的一个环节,但实际上清空垃圾桶、重新分配释放的内存同样也需要时间。重新分配还可能会导致应用程序暂停工作。垃圾收集器完成标记阶段和开始清除(重新分配)之间的接近程度称作临近性。

public static function pauseForGCIfCollectionImminent(imminence:Number = 0.75):void 是 Flash Player 11 和 AIR 3 中提供的一种新型方法,可让您向垃圾收集器建议完成标记和执行收集的有效时间(ActionScript 引用中的 API 入口)。将可能出现的暂停安排在用户不会察觉的时段有助于实现更加卓越的用户体验。例如,游戏可能会在完成某个游戏级别时调用此函数,因而减少了游戏过程中发生暂停的状况。

传递到此方法的临近性值将被拿来与该垃圾收集器处于标记阶段的值进行比较。如果传递的值小于收集器的临近性值,那么标记和清除将同时完成并导致应用程序暂停。垃圾收集器必须至少完成 1/4 的过程才能识别这种收集暂停请求。传递的值较小(虽然大于 0.25)很可能会强制执行收集,并导致该应用程序暂停。传递一个较大的值会告知垃圾收集器只有在收集过程将不久发生时才完成收集。