垃圾回收

Xamarin.Android 使用 Mono 的 简单代系垃圾回收器。 这是一个标记和清扫垃圾回收器,包含两代和一个大型 对象空间,有两种类型的回收:

  • 次要集合 (收集 Gen0 堆)
  • 主要集合(收集 Gen1 和大型对象空间堆)。

注意

如果没有通过 GC 显式收集。Collect() 集合 是按需的,基于堆分配。 这不是引用计数系统;一旦没有未完成的引用或范围退出,对象将不会立即收集。 当次要堆因新分配内存不足时,GC 将运行。 如果没有分配,则不会运行。

次要集合便宜且频繁,用于收集最近分配的对象和死对象。 在每几 MB 分配的对象之后执行次要集合。 可以通过调用 GC 手动执行次要集合。收集 (0)

主要集合昂贵且频率较低,用于回收所有死对象。 在内存耗尽当前堆大小(调整堆大小之前),将执行主要集合。 可以通过调用 GC 手动执行主要集合。收集 () 或通过调用 GC。使用参数 GC 收集 (int)。MaxGeneration。

跨 VM 对象集合

对象类型有三类。

  • 托管对象:不继承自 Java.Lang.Object 的类型,例如 System.String。 这些通常由 GC 收集。

  • Java 对象:Android 运行时 VM 中存在的但不向 Mono VM 公开的 Java 类型。 这些都是无聊的,不会进一步讨论。 这些操作通常由 Android 运行时 VM 收集。

  • 对等对象:实现 IJavaObject 的类型,例如所有 Java.Lang.ObjectJava.Lang.Throwable 子类。 这些类型的实例有两个托管对等和一个本机对等。 托管对等是 C# 类的实例。 本机对等是 Android 运行时 VM 中 Java 类的实例,C# IJavaObject.Handle 属性包含对本机对等的 JNI 全局引用。

有两种类型的本机对等:

由于 Xamarin.Android 进程中有两个 VM,因此有两种类型的垃圾回收:

  • Android 运行时集合
  • Mono 集合

Android 运行时集合正常运行,但需要注意:JNI 全局引用被视为 GC 根。 因此,如果存在保留到 Android 运行时 VM 对象的 JNI 全局引用,则无法收集该对象,即使该对象符合收集条件。

Mono 集合是乐趣发生的地方。 托管对象通常收集。 通过执行以下过程收集对等对象:

  1. 符合 Mono 集合条件的所有对等对象都将其 JNI 全局引用替换为 JNI 弱全局引用。

  2. 调用 Android 运行时 VM GC。 可以收集任何本机对等实例。

  3. 检查在 (1) 中创建的 JNI 弱全局引用。 如果已收集弱引用,则会收集 Peer 对象。 如果未收集弱引用,则弱引用将替换为 JNI 全局引用,并且不会收集 Peer 对象。 注意:在 API 14+ 上,这意味着从 IJavaObject.Handle GC 后返回的值可能会更改。

所有这些操作的最终结果是,只要托管代码(例如存储在变量中 static )或 Java 代码引用,对等对象的实例就会生存。 此外,本机对等的生存期将扩展到其他生存期之外,因为本机对等在本机对等和托管对等都是可收集的之前无法收集的。

对象周期

对等对象在 Android 运行时和 Mono VM 的逻辑上都存在。 例如, Android.App.Activity 托管对等实例将具有相应的 android.app.Activity 框架对等 Java 实例。 从 Java.Lang.Object 继承的所有对象都可以在这两个 VM 中具有表示形式。

这两个 VM 中具有表示形式的所有对象都将具有与仅存在于单个 VM 中的对象(如 a System.Collections.Generic.List<int>)相比,这些对象将具有扩展的生存期。 调用 GC。收集 不一定收集这些对象,因为 Xamarin.Android GC 需要在收集对象之前确保该对象不会被任一 VM 引用。

若要缩短对象生存期,应调用 Java.Lang.Object.Dispose()。 这将通过释放全局引用来手动“将两个 VM 之间的对象连接”化“,从而允许更快地收集对象。

自动集合

从版本 4.1.0 开始,Xamarin.Android 会在超过 gref 阈值时自动执行完整的 GC。 此阈值是平台已知最大 gref 的 90%:仿真器 (2000 max) 上的 1800 grefs 和硬件上的 46800 gref (最大 52000 个)。 注意:Xamarin.Android 仅计算 Android.Runtime.JNIEnv 创建的 grefs,并且不会知道进程中创建的任何其他 gref。 这只是启发式的

执行自动收集时,将输出类似于下面的消息以调试日志:

I/monodroid-gc(PID): 46800 outstanding GREFs. Performing a full GC!

出现这种情况是不确定的,可能发生在不合时宜的时间(例如在图形呈现的中间)。 如果看到此消息,可能需要在其他位置执行显式集合,或者可能想要尝试 减少对等对象的生存期。

GC 桥选项

Xamarin.Android 通过 Android 和 Android 运行时提供透明内存管理。 它作为单一垃圾回收器(称为 GC Bridge)的扩展实现。

GC Bridge 在 Mono 垃圾回收期间工作,并确定哪些对等对象需要通过 Android 运行时堆验证其“实时性”。 GC 桥通过执行以下步骤(按顺序)做出此决定:

  1. 将无法访问的对等对象的 mono 引用图引入到它们所表示的 Java 对象中。

  2. 执行 Java GC。

  3. 验证哪些对象真的已死。

这个复杂的过程使子类 Java.Lang.Object 能够自由引用任何对象;它消除了可绑定到 C# 的 Java 对象的任何限制。 由于这种复杂性,网桥过程可能非常昂贵,并且可能会导致应用程序中明显暂停。 如果应用程序遇到重大暂停,值得调查以下三个 GC Bridge 实现之一:

  • Tarjan - 基于 Robert Tarjan 算法和向后引用传播的 GC 桥的全新设计。 它在我们的模拟工作负荷下具有最佳性能,但它也具有实验性代码的更大份额。

  • 新增 - 对原始代码进行重大改革,修复了两个二次行为实例,但保留了核心算法(基于 Kosaraju 的算法 来查找紧密连接的组件)。

  • - 原始实现(被认为是三者中最稳定的)。 这是应用程序在可接受暂停时 GC_BRIDGE 应使用的网桥。

确定哪个 GC Bridge 最有效的唯一方法是在应用程序中进行试验和分析输出。 可通过两种方法收集数据进行基准测试:

  • 启用日志记录 - 为每个 GC Bridge 选项启用日志记录(如“配置”部分所述),然后捕获并比较每个设置中的日志输出。 GC检查每个选项的消息;特别是GC_BRIDGE消息。 对于非交互式应用程序,最多暂停 150 毫秒是可容忍的,但对于非常交互式的应用程序(如游戏)暂停超过 60 毫秒是个问题。

  • 启用桥帐 - 桥帐将显示桥牌过程中涉及的每个对象所指向的对象的平均成本。 按大小对此信息进行排序将提供包含最大额外对象的提示。

默认设置为 Tarjan。 如果发现回归,可能会发现有必要将此选项 设置为 Old。 此外,如果 Tarjan 不提高性能,则可以选择使用更稳定的选项。

若要指定GC_BRIDGE应用程序应使用、传递bridge-implementation=oldbridge-implementation=newbridge-implementation=tarjan传递给MONO_GC_PARAMS环境变量的选项。 这是通过使用生成操作AndroidEnvironment将新文件添加到项目来实现的。 例如:

MONO_GC_PARAMS=bridge-implementation=tarjan

有关详细信息,请参阅配置

帮助 GC

有多种方法可帮助 GC 减少内存使用和收集时间。

释放对等实例

GC 具有进程不完整的视图,在内存不足时可能无法运行,因为 GC 不知道内存不足。

例如,Java.Lang.Object 类型或派生类型的实例大小至少为 20 字节(可能会更改而不通知,等等)。 托管可调用包装器 不会添加其他实例成员,因此,如果你有一个 引用 10MB 内存 blob 的 Android.Graphics.Bitmap 实例,Xamarin.Android 的 GC 将不知道 - GC 将看到一个 20 字节对象,并且无法确定它已链接到 Android 运行时分配的对象,该对象将保持 10MB 内存处于活动状态。

经常需要帮助 GC。 不幸的是, GC。AddMemoryPressure()GC。不支持 RemoveMemoryPressure(), 因此,如果你 知道 刚刚释放了一个大型 Java 分配的对象图,则可能需要手动调用 GC。Collect() 提示 GC 释放 Java 端内存,或者可以显式释放 Java.Lang.Object 子类,打破托管可调用包装器与 Java 实例之间的映射。

注意

处理Java.Lang.Object子类实例时,必须格外小心。

若要最大程度地减少内存损坏的可能性,在调用 Dispose()时遵循以下准则。

在多个线程之间共享

如果 Java 或托管实例可能在多个线程之间共享,则不应Dispose()永远共享它。 例如:Typeface.Create() 可能会返回 缓存的实例。 如果多个线程提供相同的参数,它们将获取 相同的 实例。 因此, Dispose()从一个线程对实例的处理 Typeface 可能会使其他线程失效,这可能会导致 ArgumentException来自 JNIEnv.CallVoidMethod() (等等),因为该实例已从另一个线程释放。

释放绑定 Java 类型

如果实例属于绑定 Java 类型,则只要实例不会从托管代码重复使用,并且 Java 实例不能在线程之间共享(请参阅前面的Typeface.Create()讨论),就可以释放该实例。 (做出这种决定可能很困难。下次 Java 实例进入托管代码时,将为其创建新的包装器。

当涉及到 Drawables 和其他资源密集型实例时,这通常很有用:

using (var d = Drawable.CreateFromPath ("path/to/filename"))
    imageView.SetImageDrawable (d);

上述内容是安全的,因为 Drawable.CreateFromPath() 返回的对等方将引用框架对等方,而不是用户对等方。 Dispose()块末尾的using调用将中断托管的 Drawable 实例和框架 Drawable 实例之间的关系,允许在 Android 运行时需要时立即收集 Java 实例。 如果对等实例引用了用户对等,则这不会是安全的;此处我们使用“外部”信息知道Drawable无法引用用户对等方,因此Dispose()调用是安全的。

释放其他类型的

如果实例引用的类型不是 Java 类型的绑定(如自定义Activity),请勿调用Dispose()除非你知道该实例上没有 Java 代码将调用重写的方法。 未能这样做会导致 NotSupportedExceptions

例如,如果你有自定义单击侦听器:

partial class MyClickListener : Java.Lang.Object, View.IOnClickListener {
    // ...
}

不应释放此实例,因为 Java 将在将来尝试调用该实例上的方法:

// BAD CODE; DO NOT USE
Button b = FindViewById<Button> (Resource.Id.myButton);
using (var listener = new MyClickListener ())
    b.SetOnClickListener (listener);

使用显式检查避免异常

如果已实现 Java.Lang.Object.Dispose 重载方法,请避免接触涉及 JNI 的对象。 这样做可能会创建双重 释放 情况,使代码能够(致命)尝试访问已垃圾回收的基础 Java 对象。 这样做会产生类似于以下内容的异常:

System.ArgumentException: 'jobject' must not be IntPtr.Zero.
Parameter name: jobject
at Android.Runtime.JNIEnv.CallVoidMethod

当第一次释放对象会导致成员变为 null 时,通常会发生这种情况,然后对此 null 成员的后续访问尝试会导致引发异常。 具体而言,对象 Handle (将托管实例链接到其基础 Java 实例)在第一次释放时失效,但托管代码仍尝试访问此基础 Java 实例,即使它不再可用(有关 Java 实例和托管实例之间的映射的详细信息),请参阅 托管可调用包装器

防止此异常的一个好方法是在方法中Dispose显式验证托管实例与基础 Java 实例之间的映射是否仍然有效;也就是说,在访问其成员之前,检查查看对象Handle是否为 null(IntPtr.Zero)。 例如,以下 Dispose 方法访问对象 childViews

class MyClass : Java.Lang.Object, ISomeInterface 
{
    protected override void Dispose (bool disposing)
    {
        base.Dispose (disposing);
        for (int i = 0; i < this.childViews.Count; ++i)
        {
            // ...
        }
    }
}

如果初始释放传递导致childViews无效,则for循环访问将引发一个ArgumentExceptionHandle。 通过在首次childViews访问之前添加显式 Handle null 检查,以下Dispose方法可防止发生异常:

class MyClass : Java.Lang.Object, ISomeInterface 
{
    protected override void Dispose (bool disposing)
    {
        base.Dispose (disposing);

        // Check for a null handle:
        if (this.childViews.Handle == IntPtr.Zero)
            return;

        for (int i = 0; i < this.childViews.Count; ++i)
        {
            // ...
        }
    }
}

减少引用的实例

每当在 GC 期间扫描类型或子类的 Java.Lang.Object 实例时,也必须扫描实例引用的整个 对象图 。 对象图是“根实例”引用的对象实例集, 以及 根实例引用的所有内容,以递归方式引用。

请考虑以下类:

class BadActivity : Activity {

    private List<string> strings;

    protected override void OnCreate (Bundle bundle)
    {
        base.OnCreate (bundle);

        strings.Value = new List<string> (
                Enumerable.Range (0, 10000)
                .Select(v => new string ('x', v % 1000)));
    }
}

构造时BadActivity,对象图将包含 10004 个实例(1x、1xBadActivity、1x、1x stringsstring[] 持有strings、10000x 字符串实例),每当扫描实例时BadActivity,都需要扫描所有这些实例。

这可能会影响收集时间,从而导致 GC 暂停时间增加。

可以通过减少由用户对等实例根植的对象图的大小来帮助 GC。 在上面的示例中,可以通过移动到 BadActivity.strings 不继承自 Java.Lang.Object 的单独类来完成此操作:

class HiddenReference<T> {

    static Dictionary<int, T> table = new Dictionary<int, T> ();
    static int idgen = 0;

    int id;

    public HiddenReference ()
    {
        lock (table) {
            id = idgen ++;
        }
    }

    ~HiddenReference ()
    {
        lock (table) {
            table.Remove (id);
        }
    }

    public T Value {
        get { lock (table) { return table [id]; } }
        set { lock (table) { table [id] = value; } }
    }
}

class BetterActivity : Activity {

    HiddenReference<List<string>> strings = new HiddenReference<List<string>>();

    protected override void OnCreate (Bundle bundle)
    {
        base.OnCreate (bundle);

        strings.Value = new List<string> (
                Enumerable.Range (0, 10000)
                .Select(v => new string ('x', v % 1000)));
    }
}

次要集合

可以通过调用 GC 手动执行次要集合。Collect(0) . 次要集合很便宜(与主要集合相比),但确实具有显著的固定成本,因此你不想太频繁地触发它们,并且应该有一些毫秒的暂停时间。

如果应用程序有一个“职责周期”,在该周期中完成相同的操作,则建议在任务周期结束后手动执行次要集合。 示例工作周期包括:

  • 单个游戏帧的呈现周期。
  • 与给定应用对话框的整个交互(打开、填充、关闭)
  • 刷新/同步应用数据的一组网络请求。

主要集合

可以通过调用 GC 手动执行主要集合。Collect()GC.Collect(GC.MaxGeneration).

它们应该很少执行,收集 512MB 堆时,在 Android 样式的设备上可能有一秒的暂停时间。

仅应手动调用主要集合(如果有):

诊断

若要跟踪何时创建和销毁全局引用,可以将debug.mono.log系统属性设置为包含 gref 和/或 gc。

配置

可以通过设置 MONO_GC_PARAMS 环境变量来配置 Xamarin.Android 垃圾回收器。 可以使用 AndroidEnvironment生成操作设置环境变量。

环境变量 MONO_GC_PARAMS 是以下参数的逗号分隔列表:

  • nursery-size = 大小 :设置托儿所的大小。 大小以字节为单位指定,并且必须是 2 的幂。 后缀 kmg ,可用于分别指定千字节、兆字节和千兆字节。 托儿所是第一代(二代)。 较大的托儿所通常会加快程序的速度,但显然会使用更多的内存。 默认托儿所大小为 512 kb。

  • soft-heap-limit = size :应用的目标最大托管内存消耗量。 内存使用低于指定值时,GC 会针对执行时间进行优化(集合更少)。 超出此限制,GC 针对内存使用量(更多集合)进行优化。

  • evacuation-threshold = 阈值 :设置疏散阈值(以百分比为单位)。 该值必须是 0 到 100 范围内的整数。 默认值为 66。 如果集合的扫描阶段发现特定堆块类型的占用率低于此百分比,它将在下一个主要集合中为该块类型执行复制集合,从而将占用率还原到接近 100%。 值为 0 将关闭疏散。

  • bridge-implementation = 桥实现 :这将设置 GC Bridge 选项来帮助解决 GC 性能问题。 有三个可能的值: 塔詹

  • bridge-require-precise-merge:Tarjan 桥包含一个优化,在极少数情况下,可能会导致在对象首次成为垃圾后收集一个 GC。 包括此选项会禁用该优化,使 GCS 更具可预测性,但可能更慢。

例如,若要将 GC 配置为堆大小限制为 128MB,请将包含内容的生成操作AndroidEnvironment添加到项目中

MONO_GC_PARAMS=soft-heap-limit=128m