ガベージ コレクション

Xamarin.Android では、Mono の Simple Generational ガベージ コレクターが使用されます。 これは、2 種類のコレクションを含む、2 つの世代と大きなオブジェクト空間持つマークアンドスイープ ガベージ コレクターです。

  • マイナー コレクション (Gen0 ヒープを収集)
  • メジャー コレクション (Gen1 と大きなオブジェクト領域ヒープを収集します)。

Note

GC による 明示的なコレクションがない場合。Collect() コレクションは、 ヒープ割り当てに基づいてオンデマンドで実行されます。 これは参照カウント システムではありません。未処理の参照がない場合、またはスコープが終了すると、オブジェクトは収集されません。 新しい割り当てのためにマイナー ヒープのメモリが不足すると、GC が実行されます。 割り当てがない場合、実行されません。

マイナー コレクションは安価で頻繁であり、最近割り当てられたオブジェクトとデッド オブジェクトを収集するために使用されます。 マイナー コレクションは、割り当てられたオブジェクトの数MB (メガバイト)ごとに実行されます。 マイナー コレクションは、GC を呼び出 すことによって手動で実行できます。収集 (0)

メジャー コレクションはコストが高く、頻度が低く、すべてのデッド オブジェクトを回収するために使用されます。 メジャー コレクションは、(ヒープのサイズを変更する前に) 現在のヒープ サイズに対してメモリが使い果たされると実行されます。 メジャー コレクションは、GC を呼び出すことによって手動で実行できます。() を収集するか、GC を呼び出します。引数 GC を使用して (int) を収集します。MaxGeneration。

VM 間オブジェクト コレクション

オブジェクトの種類には 3 つのカテゴリがあります。

  • マネージド オブジェクト: Java.Lang.Object から継承されない(System.String など)。 これらは通常、GC によって収集されます。

  • Java オブジェクト: Android ランタイム VM 内に存在するが Mono VM に公開されていない Java 型。 これらは退屈であり、これ以上説明しません。 これらは通常、Android ランタイム VM によって収集されます。

  • ピア オブジェクト: IJavaObject を実装する型 (すべての Java.Lang.Object サブクラスや Java.Lang.Throwable サブクラスなど)。 これらの型のインスタンスには、マネージド ピアとネイティブ ピア"半分" が 2 つあります。 マネージド ピアは、C# クラスのインスタンスです。 ネイティブ ピアは Android ランタイム VM 内の Java クラスのインスタンスであり、C# IJavaObject.Handle プロパティにはネイティブ ピアへの JNI グローバル参照が含まれています。

ネイティブ ピアには、次の 2 種類があります。

Xamarin.Android プロセス内には 2 つの VM があるため、次の 2 種類のガベージ コレクションがあります。

  • 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 以降では、GC の後に返される IJavaObject.Handle 値が変更される可能性があることを意味します。

このすべての最終的な結果は、Peer オブジェクトのインスタンスが、マネージド コード (変数に格納されているなど) または Java コードによって参照されている static 限り存続します。 さらに、ネイティブ ピアとマネージド ピアの両方が収集可能になるまで、ネイティブ ピアは収集できないため、ネイティブ ピアの有効期間はそれ以外の場合の有効期間を超えて拡張されます。

オブジェクト サイクル

ピア オブジェクトは、Android ランタイムと Mono VM の両方に論理的に存在します。 たとえば、 Android.App.Activity マネージド ピア インスタンスには、対応する android.app.Activity Framework ピア Java インスタンスがあります。 Java.Lang.Object から 継承するすべてのオブジェクト には、両方の VM 内に表現が含まれている必要があります。

両方の VM で表現を持つすべてのオブジェクトは、1 つの VM 内にのみ存在するオブジェクト (たとえば System.Collections.Generic.List<int>、 GC の呼び出し Xamarin.Android GC では、収集する前にオブジェクトがいずれかの VM によって参照されないようにする必要があるため、収集はこれらのオブジェクトを必ずしも収集する必要はありません。

オブジェクトの有効期間を短縮するには、 Java.Lang.Object.Dispose() を呼び出す必要があります。 これにより、グローバル参照を解放することで、2 つの VM 間のオブジェクトの接続が手動で "切断" されるため、オブジェクトをより高速に収集できます。

自動コレクション

リリース 4.1.0 以降では、Gref しきい値を超えると、Xamarin.Android によって完全な GC が自動的に実行されます。 このしきい値は、プラットフォームの既知の最大 grefs の 90% です。エミュレーターでは 1800 grefs (最大 2000)、ハードウェアでは 46800 grefs (最大 52000) です。 注: Xamarin.Android では、Android.Runtime.JNIEnv によって作成された grefs のみがカウントされ、プロセスで作成された他の grefs については認識されません。 これはヒューリスティック のみです

自動収集が実行されると、次のようなメッセージがデバッグ ログに出力されます。

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

この発生は非決定的であり、不都合な時間 (グラフィックス レンダリングの途中など) に発生する可能性があります。 このメッセージが表示された場合は、他の場所で明示的なコレクションを実行するか、ピア オブジェクトの有効期間を短縮したい場合があります。

GC ブリッジ オプション

Xamarin.Android では、Android と Android ランタイムを使用した透過的なメモリ管理が提供されます。 これは、GC ブリッジと呼ばれる Mono ガベージ コレクターの拡張機能として実装されます。

GC ブリッジは Mono ガベージ コレクション中に機能し、Android ランタイム ヒープで検証された "ライブ性" が必要なピア オブジェクトを確認します。 GC ブリッジは、次の手順 (順番に) を実行して、この決定を行います。

  1. 到達できないピア オブジェクトのモノラル参照グラフを、それらが表す Java オブジェクトに誘導します。

  2. Java GC を実行します。

  3. どのオブジェクトが本当に死んでいるかを確認します。

この複雑なプロセスは、サブクラスが任意の Java.Lang.Object オブジェクトを自由に参照できるようにするためです。Java オブジェクトを C# にバインドできる制限を取り除きます。 この複雑さのため、ブリッジ プロセスは非常にコストがかかり、アプリケーションで顕著な一時停止を引き起こす可能性があります。 アプリケーションで大幅な一時停止が発生している場合は、次の 3 つの GC ブリッジ実装のいずれかを調査する価値があります。

  • Tarjan - Robert Tarjan のアルゴリズムと後方参照伝達に基づく GC ブリッジのまったく新しい設計。 これは、シミュレートされたワークロードで最高のパフォーマンスを発揮しますが、実験用コードのシェアも大きくなります。

  • 新規 - 元のコードを大幅に見直し、2 次的な動作の 2 つのインスタンスを修正しますが、コア アルゴリズムを維持します (厳密に接続されたコンポーネントを見つけるためのKosarajuのアルゴリズムに基づきます)。

  • 古い - 元の実装 (3 つのうち最も安定と見なされます)。 これは、一時停止が許容される場合にアプリケーションが GC_BRIDGE 使用する必要があるブリッジです。

どの GC ブリッジが最適かを判断する唯一の方法は、アプリケーションで実験し、出力を分析することです。 ベンチマークのためにデータを収集するには、次の 2 つの方法があります。

  • ログ記録を有効にする - 各 GC ブリッジ オプションのログ記録 (構成セクションで説明) を有効にしてから、各設定のログ出力をキャプチャして比較します。 各オプション ( GC 特にメッセージ) のメッセージを GC_BRIDGE 調べます。 非対話型アプリケーションの場合、最大 150 ミリ秒の一時停止は許容できますが、非常に対話型のアプリケーション (ゲームなど) では 60 ミリ秒を超える一時停止が問題になります。

  • ブリッジ アカウンティング を有効にする - ブリッジ 会計では、ブリッジ プロセスに関係する各オブジェクトが指すオブジェクトの平均コストが表示されます。 この情報をサイズで並べ替えることで、最大の量の余分なオブジェクトを保持しているものに関するヒントが提供されます。

既定の設定は Tarjan です。 回帰が見つかると、このオプションを [古い] に設定する必要がある場合があります。 また、Tarjanがパフォーマンスの向上を生み出さない場合は、より安定した古いオプションを使用することもできます。

アプリケーションで使用するGC_BRIDGEオプションを指定するには、環境変数を渡すか、bridge-implementation=new環境変数bridge-implementation=tarjanMONO_GC_PARAMSbridge-implementation=oldします。 これは、ビルド アクションAndroidEnvironmentを使用して新しいファイルをプロジェクトに追加することによって実現されます。 次に例を示します。

MONO_GC_PARAMS=bridge-implementation=tarjan

詳細については、構成に関するページを参照してください。

GC の支援

GC を使用してメモリ使用量と収集時間を短縮するには、複数の方法があります。

ピア インスタンスの破棄

GC はプロセスの不完全なビューを持ち、メモリが少ない場合は実行されない可能性があります。これは、GC がメモリが不足していることを認識していないためです。

たとえば、Java.Lang.Object 型または派生型のインスタンスのサイズは 20 バイト以上です (予告なく変更される場合など)。 マネージド呼び出し可能ラッパーは追加のインスタンス メンバーを追加しないため、メモリの 10 MB (メガバイト) BLOB を参照する Android.Graphics.Bitmap インスタンスがある場合、Xamarin.Android の GC では、20 バイトのオブジェクトが表示され、メモリが 10 MB (メガバイト)維持されている Android ランタイム割り当てオブジェクトにリンクされていることを判断できません。

多くの場合、GC を支援する必要があります。 残念ながら、GC。AddMemoryPressure()GC。RemoveMemoryPressure() はサポートされていないため、Java で割り当てられた大きなオブジェクト グラフを解放しただけのことがわかっている場合は、GC を手動で呼び出す必要があります。Gc に Java 側のメモリを解放するように求める Collect() か、Java.Lang.Object サブクラスを明示的に破棄して、マネージド呼び出し可能ラッパーと Java インスタンス間のマッピングを中断できます。

Note

サブクラス インスタンスを破棄Java.Lang.Objectする場合は、十分に注意する必要があります。

メモリ破損の可能性を最小限に抑えるには、呼び出 Dispose()すときに次のガイドラインに従います。

複数のスレッド間での共有

Java またはマネージド インスタンスを複数のスレッド間で共有できる場合は、d にすることはできません。Dispose() たとえば、 のように指定します。Typeface.Create()は、キャッシュされたインスタンス返す可能性があります。 複数のスレッドが同じ引数を指定すると、同じインスタンスが取得されます。 その結果、 Dispose()1 つのスレッドからインスタンスを削除すると、他の Typeface スレッドが無効になる可能性があります。その結果 ArgumentException、インスタンスが別の JNIEnv.CallVoidMethod() スレッドから破棄されたために、(特に) s が発生する可能性があります。

バインドされた Java 型の破棄

インスタンスがバインドされた Java 型の場合、インスタンスがマネージド コードから再利用されず、Java インスタンスをスレッド間で共有できない限り、インスタンスを破棄できます (前Typeface.Create()の説明を参照)。 (この決定を行うのは難しい場合があります。次に Java インスタンスがマネージド コードを入力すると、新しいラッパーが作成されます。

これは、Drawables やその他のリソース負荷の高いインスタンスに関しては、多くの場合に役立ちます。

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

上記は安全です。Drawable.CreateFromPath() が返すピアは、ユーザー ピアではなく Framework ピアを参照するためです。 ブロックの最後の呼び出しによりDispose()、マネージド Drawable インスタンスとフレームワーク Drawable インスタンス間のusing関係が中断され、Android ランタイムが必要になるとすぐに Java インスタンスを収集できるようになります。 ピア インスタンスがユーザー ピアを参照している場合、これは安全ではありません。ここでは、"外部" 情報を使用して、ユーザー ピアを参照できないことを認識DrawableしているためDispose()、呼び出しは安全です。

他の型の破棄

インスタンスが Java 型 (カスタムなど) のバインドではない型を参照している場合は、そのインスタンスでオーバーライドされたメソッドを呼び出Dispose()す Java コードがないことがわかっている場合を除き、呼び出さないでくださいActivity これを行わないと、s が発生 NotSupportedExceptionします

たとえば、カスタム クリック リスナーがある場合は、次のようになります。

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 インスタンスとマネージド インスタンス間のマッピングの詳細については、マネージド呼び出し可能ラッパーを参照してください)。

この例外を回避する良い方法は、マネージド インスタンスと基になる Java インスタンス間のマッピングがまだ有効であることをメソッドでDispose明示的に確認することです。つまり、オブジェクトのメンバーにアクセスする前に、オブジェクト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)
        {
            // ...
        }
    }
}

最初の dispose パスが原因で childViews 無効 Handleな場合、 for ループ アクセスは ArgumentException. 最初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 インスタンス (1xBadActivity、1xstrings、1x、10000x string[] の文字列インスタンスによってstrings保持されます) が含まれ、インスタンスがスキャンされるたびにBadActivityすべてのインスタンスをスキャンする必要があります。

これにより、収集時間に悪影響が及び、GC の一時停止時間が増加する可能性があります。

ユーザー ピア インスタンスによってルート化されるオブジェクト グラフのサイズを小さくすることで、GC を支援できます。 上記の例では、Java.Lang.Object から継承されない別のクラスに移動 BadActivity.strings することで、これを行うことができます。

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)。 マイナー コレクションは (メジャー コレクションと比較して) 安価ですが、固定コストが大きいため、あまり頻繁にトリガーしたくないので、数ミリ秒の一時停止時間が必要です。

アプリケーションに同じ処理が繰り返し実行される "デューティ サイクル" がある場合は、デューティ サイクルが終了したら、手動でマイナー コレクションを実行することをお勧めします。 デューティ サイクルの例を次に示します。

  • 1 つのゲーム フレームのレンダリング サイクル。
  • 特定のアプリ ダイアログとの対話全体 (開く、入力する、閉じる)
  • アプリ データを更新/同期するためのネットワーク要求のグループ。

メジャー コレクション

メジャー コレクションは、GC を呼び出 すことによって手動で実行できます。Collect() または GC.Collect(GC.MaxGeneration).

これらはまれに実行する必要があり、512 MB (メガバイト) ヒープを収集するときに Android スタイルのデバイスで 1 秒間の一時停止時間が発生する可能性があります。

メジャー コレクションは、次の場合にのみ手動で呼び出す必要があります。

  • 長いデューティ サイクルの終了時に、長い一時停止が発生してもユーザーに問題は発生しません。

  • オーバーライドされた Android.App.Activity.OnLowMemory() メソッド内。

診断

グローバル参照が作成および破棄されるタイミングを追跡するには、gref gc含むdebug.mono.logシステム プロパティを設定します。

構成

Xamarin.Android ガベージ コレクターは、環境変数を MONO_GC_PARAMS 設定することで構成できます。 環境変数は、AndroidEnvironmentビルド アクションで設定できます。

環境変数は MONO_GC_PARAMS 、次のパラメーターのコンマ区切りの一覧です。

  • nursery-size = size : 育苗のサイズを設定します。 サイズはバイト単位で指定され、2 の累乗である必要があります。 サフィックス kmg 。また、それぞれキロ、メガ、ギガバイトを指定するために使用できます。 保育所は第一世代 (2 人) です。 大規模な保育所は、通常、プログラムを高速化しますが、明らかにより多くのメモリを使用します。 既定の保育サイズは 512 kb です。

  • soft-heap-limit = size : アプリのターゲットの最大マネージド メモリ消費量。 メモリ使用量が指定した値を下回ると、GC は実行時間に合わせて最適化されます (コレクションの数が少なくなります)。 この制限を超えて、GC はメモリ使用量 (より多くのコレクション) 用に最適化されています。

  • evacuation-threshold = しきい値 : 避難のしきい値をパーセントで設定します。 値は、0 ~ 100 の範囲の整数である必要があります。 既定値は 66 です。 コレクションのスイープ フェーズで、特定のヒープ ブロックの種類の占有率がこの割合より小さいことが判明した場合、次のメジャー コレクションでそのブロックの種類のコピー コレクションが実行され、占有率が 100% 近くに復元されます。 値 0 を指定すると、退避がオフになります。

  • bridge-implementation = ブリッジの実装 : これにより、GC パフォーマンスの問題に対処するために GC ブリッジ オプションが設定されます。 古い値、新しい値、tarjanの 3 つを指定できます。

  • bridge-require-precise-merge:Tarjan ブリッジには最適化が含まれており、まれに、オブジェクトが最初にガベージになった後に 1 つの GC を収集する可能性があります。 このオプションを含めると、その最適化が無効になり、PC の予測は可能になりますが、遅くなる可能性があります。

たとえば、ヒープ サイズの制限が 128 に設定するように GC を構成するにはMB (メガバイト)コンテンツを含むビルド アクションAndroidEnvironmentを使用して新しいファイルをプロジェクトに追加します。

MONO_GC_PARAMS=soft-heap-limit=128m