垃圾回收
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.Object 和 Java.Lang.Throwable 子类。 这些类型的实例有两个托管对等和一个本机对等。 托管对等是 C# 类的实例。 本机对等是 Android 运行时 VM 中 Java 类的实例,C# IJavaObject.Handle 属性包含对本机对等的 JNI 全局引用。
有两种类型的本机对等:
框架对等方 :“普通”Java 类型,这些类型不了解 Xamarin.Android,例如 android.content.Context。
由于 Xamarin.Android 进程中有两个 VM,因此有两种类型的垃圾回收:
- Android 运行时集合
- Mono 集合
Android 运行时集合正常运行,但需要注意:JNI 全局引用被视为 GC 根。 因此,如果存在保留到 Android 运行时 VM 对象的 JNI 全局引用,则无法收集该对象,即使该对象符合收集条件。
Mono 集合是乐趣发生的地方。 托管对象通常收集。 通过执行以下过程收集对等对象:
符合 Mono 集合条件的所有对等对象都将其 JNI 全局引用替换为 JNI 弱全局引用。
调用 Android 运行时 VM GC。 可以收集任何本机对等实例。
检查在 (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 桥通过执行以下步骤(按顺序)做出此决定:
将无法访问的对等对象的 mono 引用图引入到它们所表示的 Java 对象中。
执行 Java GC。
验证哪些对象真的已死。
这个复杂的过程使子类 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=old
bridge-implementation=new
或bridge-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 代码将调用重写的方法。
未能这样做会导致 NotSupportedException
s。
例如,如果你有自定义单击侦听器:
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
循环访问将引发一个ArgumentException
Handle
。 通过在首次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 strings
string[]
持有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 样式的设备上可能有一秒的暂停时间。
仅应手动调用主要集合(如果有):
在较长的执勤周期结束时,当长时间暂停不会向用户提出问题时。
在重写的 Android.App.Activity.OnLowMemory() 方法中。
诊断
若要跟踪何时创建和销毁全局引用,可以将debug.mono.log系统属性设置为包含 gref 和/或 gc。
配置
可以通过设置 MONO_GC_PARAMS
环境变量来配置 Xamarin.Android 垃圾回收器。 可以使用 AndroidEnvironment 的生成操作设置环境变量。
环境变量 MONO_GC_PARAMS
是以下参数的逗号分隔列表:
nursery-size
= 大小 :设置托儿所的大小。 大小以字节为单位指定,并且必须是 2 的幂。 后缀k
m
g
,可用于分别指定千字节、兆字节和千兆字节。 托儿所是第一代(二代)。 较大的托儿所通常会加快程序的速度,但显然会使用更多的内存。 默认托儿所大小为 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