Flash Player 和 Adobe AIR 垃圾收集内幕

  • Michelle Yaiser

2011 年 11 月 17 日

话题:DevOps语言 & 开发架构

目录

需求

预备知识

本文适用于中级和高级 ActionScript 开发人员。需要对面向对象的编程概念和 ActionScript 3 开发具有中等程度的理解。

用户水平

中级

需要的产品

所有应用程序都要管理内存。应用程序的内存管理包括用于确定何时分配内存,分配多少内存,何时将内容放入回收站,以及何时清空回收站的准则。MMgc 是 Flash Player 用于几乎所有内存分配工作的通用内存管理器。理解 MMgc 如何管理内存是优化您的代码和您应用程序的性能的一个重要部分。

垃圾收集器自动回收的内存被视为“受管理的内存”。垃圾收集器确定内存何时不再被应用程序使用并回收它。本文分析 Flash Player 11 和 AIR 3 中的内存分配、垃圾收集流程和新的 pauseForGCIfCollectionImminent()API。

内存分配

Flash Player 使用一个页面分配程序(GCheap)来从 OD 分配大块(几 MB)的内存。Gcheap 然后将大内存块分解为较小的 4K 页面,并根据需要将这些页面提供给垃圾收集(GC)内存管理器。

图 1. GCHeap 从 OS 分配内存,将它分解为 4K 的页面,并将这些页面提供给 GC。

GC 然后使用这些 4K 页面为系统中不大于 2K 的对象提供内存。

图 2. 4K 页面由 GC 分配给小于 2K 的对象

对于大于 2K 的对象(位图、视频、文件等),GCHeap 向一个大型的内存分配程序提供一组连续的 4K 内存块。

当一个大内存块中几乎所有 4K 页面都分配了时,Flash Player 运行垃圾收集来回收未使用的内存,然后 GCHeap 尝试从 OS 分配更多内存。换句话说,垃圾收集仅由内存分配触发。这一事实很重要,在测试和分析期间一定要记住,因为它意味着空闲应用程序的内存使用从不会改变。

堆和堆栈

堆是分配给在运行时创建或初始化的任何对象的内存。堆上的对象会一直存在到它们被垃圾收集。

图 3. 对象 A 存在于堆上。它由堆栈上的局部变量 o 引用。(图字:堆栈内存 堆内存)

堆栈是存储在编译时定义的所有变量的内存。堆栈内存以一种顺序方式使用和重用。推送操作将一些内容添加到堆栈顶部。弹出操作从堆栈顶部删除一些内容。访问堆栈中间的内容的唯一方式是删除它上方的所有内容。

局部方法变量、参数和关于在一个方法完成时返回到何处的信息,在方法运行时被推送到堆栈上。对堆栈的更改发生得非常快。对象的堆栈引用可能非常短暂。这些对象引用可能存在于堆栈上,但分配给这些对象的内存来自堆。

图 4. 局部变量在定义时被推送到堆栈上。关于在一个方法完成时返回何处的信息也推送到堆栈上。

Flash 运行时垃圾收集实现

Flash Player 和 AIR 结合使用延迟的引用计数和保守的标记并清除(mark-and-sweep)方法。

延迟的引用计数

在延迟的引用计数中,堆和堆栈引用之间存在区别。因为堆栈变化很快,并可能包含非常短暂的引用,所以引用计数不会在堆栈引用上执行。而在堆上为引用维护引用计数。

图 5. 对象会跟踪它们拥有多少个引用。

堆上的每个对象会跟踪指向它的信息数量。每次您创建一个对象的引用,该对象的引用计数就会递增。当您删除一个引用时,该对象的引用计数会递减。如果对象的引用计数为 0(没有任何信息指向它),它会被添加到零计数表(Zero Count Table,ZCT)中。当 ZCT 填满后,就会扫描堆栈以查找任何从堆栈到 ZCT 上的对象的引用。ZCT 上任何没有堆栈引用的对象都会被删除。

延迟引用计数的一个问题是循环引用。如果 ObjectA 和 ObjectB 彼此引用,而系统中没有其他对象指向它们,它们将从不会拥有一个零引用计数,因此从不满足使用引用计数进行垃圾收集的资格。这时可以使用“标记并清除”的垃圾收集方法。

图 6. Object A 和 Object B 彼此引用,但没有其他引用。

标记 / 清除

在 Flash Player 或 AIR 中运行的应用程序具有多个 GCRoot。您可以将 GCRoot 视为一个树的一部分,它将应用程序的对象当作树枝。舞台是一个 GCRoot。加载程序是 GCRoot。某些菜单是 GCRoot。让在供应用程序使用的每个对象可从应用程序内的一个 GCRoot 访问。GCRoot 从不会被垃圾收集。

应用程序中的每个对象有一个“标记位”。当垃圾收集的标记阶段开始时,所有这些标记位会被清除。MMgc 会跟踪应用程序中的所有 GCRoot。垃圾收集器首先从这些根开始,跟踪每个对象并为它到达的每个对象设置标记位。任何不再能够从任何根到达的对象也不再能够从应用程序的任何地方到达——它的标记位不会在标记阶段设置。收集器完成对它找到的所有对象进行标记之后,就会开始清除阶段。任何没有设置标记位的对象都会被销毁,它的内存会被回收。

图 7. 一个循环引用中的对象没有被标记。

图 7 显示,每个可从 Gcroot 到达的对象都设置了自己的标记位(蓝色)。一个循环引用中的两个对象(ObjectA 和 ObjectB)不可从 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. 若引用在标记期间不会被跟踪。

在这种情况下,到 Object B 的唯一路径是弱的。在跟踪期间将不会经过它,因此 Object B 不会被标记,并会被收集。但是,如果还有另一个到 Object B 的强路径,Object B 将被标记并被持久化。

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

您应该始终清理未使用的引用,从字典删除未使用的项,以及使用 removeEventListener()。但是,有时清理未使用的引用不切实际或无法做到。比如在您的类在您不知情的情况下实例化和销毁时——项渲染器就是通过这种方式使用的。在这些情况下,维持对象的若引用将允许 Flash Player 最终删除它们并回收内存。

保守收集

MMgc 被视为一种保守的标记 / 清除收集器。MMgc 无法确定内存中的某些值是对象指针(内存地址)还是数字值。为了避免意外地收集值可能指向的对象,MMgc 假设每个值都可以是一个指针。因此,一些没有实际被指向的对象将从不被收集,将被视为一种内存泄漏。尽管您希望最小化内存泄漏以优化性能,但由保守的 GC 所导致的偶然泄漏可能是随机的,不会随时间增长,并且对应用程序性能的影响比开发人员导致的泄漏小得多。

增量收集

不幸的是,垃圾收集可导致 Flash Player 在收集过程完成时定期暂停。这种暂停与应用程序当前运行的内存量成正比。它可能比希望的时间更长,在一些程序中可以察觉到。

标记阶段是垃圾收集过程中最消耗时间的部分。由于此事实,标记过程使用一个动作队列和一个 3 色算法增量化了。该队列在标记增量之间维护标记状态。

表 1. 3 色算法

clip_image013[4]

黑色对象已标记,不再位于队列中。

clip_image015

灰色对象位于队列中,还未被标记。

clip_image017

白色对象既未标记也不在队列中。

在标记阶段的开始,所以 GCRoot 被推送到队列中并变为灰色。

图 10. GCRoot 在推送到工作队列中时变成灰色。

随着标记过程的继续,标记的对象变为灰色,并从工作队列删除。

图 11. 标记的对象是黑色的,不再在工作队列中。

此过程会正常继续进行,直到将一个新对象(白色)添加到一个黑色对象上。当发生此情况时,白色对象从不会设置它的标记位,因为它们的 GCRoot 已标记。不设置它们的标记位,它们将在清除阶段被垃圾收集。

图 12. 新对象被添加到以前标记的对象上。

要预防此问题,可以在 MMgc 中使用一个白色边界来强制将任何添加到黑色对象上的白色对象立即添加到工作队列中。

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

通过使用工作队列和 3 色算法,可开始和停止标记阶段来帮助避免长时间、意外的垃圾收集暂停。

迫近度

标记阶段可能是垃圾收集中最耗时的部分,但实际上清空回收站(重新分配空闲内存)也比较耗时。重新分配还可能导致应用程序暂停。垃圾收集器离标记阶段的完成和清除(重新分配)阶段的开始的时间称为“迫近度(imminence)”

图 14. 迫近度(图字:标记 暂停、迫近度增长)

public static function pauseForGCIfCollectionImminent(imminence:Number = 0.75):void 是 Flash Player 11 和 AIR 3 中的一个新方法,允许您通知垃圾收集器这是完成标记和执行收集的好时机(ActionScript 参考文档中的 API 项)。计划在用户不会注意到时发生可能的暂停,这会带来更好的用户体验。例如,一个游戏可能在游戏中一个级别完成时调用此函数,进而减少在玩游戏期间发生暂停的机会。

您传递给此方法的迫近度值用于与垃圾收集器处于标记阶段中的位置进行比较。如果您传递给它的值比垃圾收集器的迫近度值小,标记和清除将同步完成并导致应用程序暂停。垃圾收集器必须处于该过程的 25% 以上,才能响应这个暂停以进行收集的请求。传递一个较小的值(但大于 0.25)很可能会强制执行收集,导致应用程序暂停。传递一个较大的值将告诉垃圾收集器只有在即将暂停时完成收集。

延伸阅读

理解内存管理和垃圾收集在 Flash Player 和 AIR 中的工作原理,将有助于您优化您的代码并开发更高性能的应用程序。请查阅 Michael Labriola 介绍垃圾收集的演示Talking Trash。阅读 Christian Cantrell 的Providing Hints to the Garbage Collector in AIR 3。您也可以阅读详细的MMgc讨论,其中包含对底层 C++ 代码的描述。

查看原文:Garbage collection internals for Flash Player and Adobe AIR

DevOps语言 & 开发架构