Bagikan melalui


Pengumpulan Sampah

Xamarin.Android menggunakan pengumpul sampah Generasi Sederhana Mono. Ini adalah pengumpul sampah mark-and-sweep dengan dua generasi dan ruang objek besar, dengan dua jenis koleksi:

  • Koleksi kecil (mengumpulkan timbunan Gen0)
  • Koleksi utama (mengumpulkan Gen1 dan timbunan ruang objek besar).

Catatan

Dengan tidak adanya koleksi eksplisit melalui GC. Koleksi Collect() sesuai permintaan, berdasarkan alokasi timbunan. Ini bukan sistem penghitungan referensi; objek tidak akan dikumpulkan segera setelah tidak ada referensi yang luar biasa, atau ketika cakupan telah keluar. GC akan berjalan ketika tumpukan kecil telah kehabisan memori untuk alokasi baru. Jika tidak ada alokasi, alokasi tidak akan berjalan.

Koleksi kecil murah dan sering, dan digunakan untuk mengumpulkan objek yang baru saja dialokasikan dan mati. Koleksi kecil dilakukan setelah setiap beberapa MB objek yang dialokasikan. Koleksi kecil dapat dilakukan secara manual dengan memanggil GC. Kumpulkan (0)

Koleksi utama mahal dan lebih jarang, dan digunakan untuk merebut kembali semua objek mati. Koleksi utama dilakukan setelah memori habis untuk ukuran tumpukan saat ini (sebelum mengubah ukuran tumpukan). Koleksi utama dapat dilakukan secara manual dengan memanggil GC. Kumpulkan () atau dengan memanggil GC. Kumpulkan (int) dengan argumen GC. MaxGeneration.

Koleksi Objek Lintas VM

Ada tiga kategori jenis objek.

  • Objek terkelola: jenis yang tidakmewarisi dari Java.Lang.Object, misalnya System.String. Ini dikumpulkan secara normal oleh GC.

  • Objek Java: Jenis Java yang ada dalam VM runtime Android tetapi tidak terekspos ke VM Mono. Ini membosankan, dan tidak akan dibahas lebih lanjut. Ini dikumpulkan secara normal oleh VM runtime Android.

  • Objek serekan: jenis yang mengimplementasikan IJavaObject , misalnya semua subkelas Java.Lang.Object dan Java.Lang.Throwable . Instans jenis ini memiliki dua "setengah" serekan terkelola dan serekan asli. Peer terkelola adalah instans kelas C#. Peer asli adalah instans kelas Java dalam VM runtime Android, dan properti C# IJavaObject.Handle berisi referensi global JNI ke peer asli.

Ada dua jenis rekan asli:

  • Framework serekan : Jenis Java "Normal" yang tidak tahu apa-apa tentang Xamarin.Android, misalnya android.content.Context.

  • Rekan pengguna : Android Callable Wrappers yang dihasilkan pada waktu build untuk setiap subkelas Java.Lang.Object yang ada dalam aplikasi.

Karena ada dua VM dalam proses Xamarin.Android, ada dua jenis pengumpulan sampah:

  • Koleksi runtime Android
  • Koleksi mono

Koleksi runtime Android beroperasi secara normal, tetapi dengan peringatan: referensi global JNI diperlakukan sebagai akar GC. Akibatnya, jika ada referensi global JNI yang memegang objek VM runtime Android, objek tidak dapat dikumpulkan, bahkan jika memenuhi syarat untuk pengumpulan.

Koleksi mono adalah tempat yang menyenangkan terjadi. Objek terkelola dikumpulkan secara normal. Objek serekan dikumpulkan dengan melakukan proses berikut:

  1. Semua objek Peer yang memenuhi syarat untuk koleksi Mono memiliki referensi global JNI mereka diganti dengan referensi global JNI lemah.

  2. VM GC runtime Android dipanggil. Setiap instans peer asli dapat dikumpulkan.

  3. Referensi global lemah JNI yang dibuat dalam (1) diperiksa. Jika referensi lemah telah dikumpulkan, maka objek Peer dikumpulkan. Jika referensi lemah belum dikumpulkan, referensi lemah diganti dengan referensi global JNI dan objek Serekan tidak dikumpulkan. Catatan: pada API 14+, ini berarti bahwa nilai yang dikembalikan dari IJavaObject.Handle dapat berubah setelah GC.

Hasil akhirnya dari semua ini adalah bahwa instans objek Peer akan hidup selama direferensikan oleh kode terkelola (misalnya disimpan dalam variabel) atau direferensikan static oleh kode Java. Selain itu, masa pakai serekan Asli akan diperpanjang melebihi apa yang akan mereka jalani, karena serekan Asli tidak akan dapat dikumpulkan sampai serekan Asli dan serekan Terkelola dapat dikumpulkan.

Siklus Objek

Objek serekan secara logis ada dalam runtime Android dan VM Mono. Misalnya, instans peer yang dikelola Android.App.Activity akan memiliki instans Java serekan kerangka kerja android.app.Activity yang sesuai. Semua objek yang mewarisi dari Java.Lang.Object dapat diharapkan memiliki representasi dalam kedua VM.

Semua objek yang memiliki representasi di kedua VM akan memiliki masa pakai yang diperpanjang dibandingkan dengan objek yang hanya ada dalam satu VM (seperti System.Collections.Generic.List<int>). Memanggil GC. Collect tidak akan selalu mengumpulkan objek ini, karena Xamarin.Android GC perlu memastikan bahwa objek tidak dirujuk oleh VM sebelum mengumpulkannya.

Untuk mempersingkat masa pakai objek, Java.Lang.Object.Dispose() harus dipanggil. Ini akan secara manual "memisahkan" koneksi pada objek antara dua VM dengan membebaskan referensi global, sehingga memungkinkan objek dikumpulkan lebih cepat.

Koleksi Otomatis

Dimulai dengan Rilis 4.1.0, Xamarin.Android secara otomatis melakukan GC penuh saat ambang batas gref dilewati. Ambang batas ini adalah 90% dari gref maksimum yang diketahui untuk platform: 1800 gref pada emulator (2000 maks), dan 46800 gref pada perangkat keras (maksimum 52000). Catatan: Xamarin.Android hanya menghitung gref yang dibuat oleh Android.Runtime.JNIEnv, dan tidak akan tahu tentang gref lain yang dibuat dalam proses. Ini hanya heuristik.

Saat koleksi otomatis dilakukan, pesan yang mirip dengan berikut ini akan dicetak ke log debug:

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

Terjadinya hal ini tidak deterministik, dan dapat terjadi pada waktu yang tidak optimal (misalnya di tengah penyajian grafis). Jika Anda melihat pesan ini, Anda mungkin ingin melakukan koleksi eksplisit di tempat lain, atau Anda mungkin ingin mencoba mengurangi masa pakai objek serekan.

Opsi Jembatan GC

Xamarin.Android menawarkan manajemen memori transparan dengan Runtime Android dan Android. Ini diimplementasikan sebagai ekstensi untuk pengumpul sampah Mono yang disebut GC Bridge.

GC Bridge bekerja selama pengumpulan sampah Mono dan mencari tahu objek serekan mana yang membutuhkan "keaktifan" mereka yang diverifikasi dengan tumpukan runtime Android. GC Bridge membuat penentuan ini dengan melakukan langkah-langkah berikut (secara berurutan):

  1. Menginduksi grafik referensi mono dari objek serekan yang tidak dapat dijangkau ke dalam objek Java yang diwakilinya.

  2. Melakukan Java GC.

  3. Verifikasi objek mana yang benar-benar mati.

Proses rumit inilah yang memungkinkan subkelas untuk secara bebas mereferensikan Java.Lang.Object objek apa pun; ini menghapus batasan apa pun di mana objek Java dapat terikat ke C#. Karena kompleksitas ini, proses jembatan bisa sangat mahal dan dapat menyebabkan jeda yang terlihat dalam aplikasi. Jika aplikasi mengalami jeda yang signifikan, ada baiknya menyelidiki salah satu dari tiga implementasi GC Bridge berikut:

  • Tarjan - Desain jembatan GC yang sepenuhnya baru berdasarkan algoritma Robert Tarjan dan penyebaran referensi mundur. Ini memiliki performa terbaik di bawah beban kerja simulasi kami, tetapi juga memiliki bagian kode eksperimental yang lebih besar.

  • Baru - Perombakan utama kode asli, memperbaiki dua instans perilaku kuadrat tetapi menjaga algoritma inti (berdasarkan algoritma Kosaraju untuk menemukan komponen yang sangat terhubung).

  • Lama - Implementasi asli (dianggap yang paling stabil dari ketiganya). Ini adalah jembatan yang harus digunakan aplikasi jika GC_BRIDGE jeda dapat diterima.

Satu-satunya cara untuk mengetahui GC Bridge mana yang paling sesuai adalah dengan bereksperimen dalam aplikasi dan menganalisis output. Ada dua cara untuk mengumpulkan data untuk tolok ukur:

  • Aktifkan pengelogan - Aktifkan pengelogan (seperti yang dijelaskan di bagian Konfigurasi ) untuk setiap opsi GC Bridge, lalu ambil dan bandingkan output log dari setiap pengaturan. GC Periksa pesan untuk setiap opsi; khususnya, GC_BRIDGE pesan. Menjeda hingga 150ms untuk aplikasi non-interaktif dapat ditoleransi, tetapi jeda di atas 60ms untuk aplikasi yang sangat interaktif (seperti game) adalah masalah.

  • Aktifkan akuntansi jembatan - Akuntansi jembatan akan menampilkan biaya rata-rata objek yang ditujukan oleh setiap objek yang terlibat dalam proses jembatan. Mengurutkan informasi ini menurut ukuran akan memberikan petunjuk tentang apa yang menyimpan jumlah objek tambahan terbesar.

Pengaturan defaultnya adalah Tarjan. Jika Anda menemukan regresi, Anda mungkin merasa perlu untuk mengatur opsi ini ke Lama. Selain itu, Anda dapat memilih untuk menggunakan opsi Lama yang lebih stabil jika Tarjan tidak menghasilkan peningkatan performa.

Untuk menentukan opsi mana GC_BRIDGE yang harus digunakan aplikasi, teruskan bridge-implementation=old, bridge-implementation=new atau bridge-implementation=tarjan ke MONO_GC_PARAMS variabel lingkungan. Ini dicapai dengan menambahkan file baru ke proyek Anda dengan tindakan Build .AndroidEnvironment Contohnya:

MONO_GC_PARAMS=bridge-implementation=tarjan

Untuk informasi lebih lanjut, lihat Konfigurasi.

Membantu GC

Ada beberapa cara untuk membantu GC mengurangi penggunaan memori dan waktu pengumpulan.

Membuang instans Peer

GC memiliki tampilan proses yang tidak lengkap dan mungkin tidak berjalan ketika memori rendah karena GC tidak tahu bahwa memori rendah.

Misalnya, instans jenis Java.Lang.Object atau jenis turunan berukuran setidaknya 20 byte (dapat berubah tanpa pemberitahuan, dll., dll.). Managed Callable Wrappers tidak menambahkan anggota instans tambahan, jadi ketika Anda memiliki instans Android.Graphics.Bitmap yang mengacu pada blob memori 10MB, GC Xamarin.Android tidak akan tahu bahwa - GC akan melihat objek 20 byte dan tidak akan dapat menentukan bahwa itu ditautkan ke objek yang dialokasikan runtime Android yang menjaga memori 10MB tetap hidup.

Sering kali diperlukan untuk membantu GC. Sayangnya, GC. AddMemoryPressure() dan GC. RemoveMemoryPressure() tidak didukung, jadi jika Anda tahu bahwa Anda baru saja membebaskan grafik objek besar yang dialokasikan Java, Anda mungkin perlu memanggil GC secara manual. Collect() untuk meminta GC merilis memori sisi Java, atau Anda dapat secara eksplisit membuang subkelas Java.Lang.Object , memecah pemetaan antara pembungkus yang dapat dipanggil terkelola dan instans Java.

Catatan

Anda harus sangat berhati-hati saat membuang Java.Lang.Object instans subkelas.

Untuk meminimalkan kemungkinan kerusakan memori, amati panduan berikut saat memanggil Dispose().

Berbagi Antara Beberapa Utas

Jika Java atau instans terkelola dapat dibagikan di antara beberapa utas, itu tidak boleh Dispose()d, pernah. Misalnya: Typeface.Create() dapat mengembalikan instans yang di-cache. Jika beberapa utas menyediakan argumen yang sama, mereka akan mendapatkan instans yang sama . Akibatnya, Dispose()ing Typeface instans dari satu utas dapat membatalkan utas lain, yang dapat mengakibatkan ArgumentExceptions dari JNIEnv.CallVoidMethod() (antara lain) karena instans dibuang dari utas lain.

Membuang Jenis Java Terikat

Jika instans memiliki jenis Java terikat, instans dapat dibuang selama instans tidak akan digunakan kembali dari kode terkelola dan instans Java tidak dapat dibagikan di antara utas (lihat diskusi sebelumnya Typeface.Create() ). (Membuat penentuan ini mungkin sulit.) Saat berikutnya instans Java memasukkan kode terkelola, pembungkus baru akan dibuat untuknya.

Ini sering berguna dalam hal Drawables dan instans berat sumber daya lainnya:

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

Hal di atas aman karena rekan yang dikembalikan Drawable.CreateFromPath() akan merujuk ke peer Framework, bukan peer Pengguna. Panggilan Dispose() di akhir using blok akan memutus hubungan antara instans Drawable dan framework Drawable terkelola, memungkinkan instans Java dikumpulkan segera setelah runtime Android perlu. Ini tidak akan aman jika instans Peer mengacu pada rekan Pengguna; di sini kita menggunakan informasi "eksternal" untuk mengetahui bahwa Drawable tidak dapat merujuk ke rekan Pengguna, dan dengan demikian Dispose() panggilan aman.

Membuang Jenis Lain

Jika instans mengacu pada jenis yang bukan pengikatan jenis Java (seperti kustom Activity), JANGAN panggil Dispose() kecuali Anda tahu bahwa tidak ada kode Java yang akan memanggil metode yang ditimpa pada instans tersebut. Kegagalan untuk melakukannya menghasilkanNotSupportedException.

Misalnya, jika Anda memiliki pendengar klik kustom:

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

Anda tidak boleh membuang instans ini, karena Java akan mencoba memanggil metode di atasnya di masa mendatang:

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

Menggunakan Pemeriksaan Eksplisit untuk Menghindari Pengecualian

Jika Anda telah menerapkan metode kelebihan beban Java.Lang.Object.Dispose , hindari menyentuh objek yang melibatkan JNI. Melakukannya dapat membuat situasi pembuangan ganda yang memungkinkan kode Anda untuk (fatal) mencoba mengakses objek Java yang mendasar yang telah dikumpulkan sampah. Melakukannya menghasilkan pengecualian yang mirip dengan yang berikut ini:

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

Situasi ini sering terjadi ketika pembuangan pertama objek menyebabkan anggota menjadi null, dan kemudian upaya akses berikutnya pada anggota null ini menyebabkan pengecualian dilemparkan. Secara khusus, objek Handle (yang menautkan instans terkelola ke instans Java yang mendasarnya) tidak valid pada pembuangan pertama, tetapi kode terkelola masih mencoba mengakses instans Java yang mendasar ini meskipun tidak lagi tersedia (lihat Pembungkus yang Dapat Dipanggil Terkelola untuk informasi selengkapnya tentang pemetaan antara instans Java dan instans terkelola).

Cara yang baik untuk mencegah pengecualian ini adalah dengan memverifikasi secara eksplisit dalam metode Anda Dispose bahwa pemetaan antara instans terkelola dan instans Java yang mendasar masih valid; yaitu, periksa apakah objek Handle null (IntPtr.Zero) sebelum mengakses anggotanya. Misalnya, metode berikut Dispose mengakses childViews objek:

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

Jika pass pembuangan awal menyebabkan childViews akses perulangan tidak valid Handle, for akses perulangan akan melempar ArgumentException. Dengan menambahkan pemeriksaan null eksplisit Handle sebelum akses pertama childViews , metode berikut Dispose mencegah pengecualian terjadi:

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)
        {
            // ...
        }
    }
}

Kurangi Instans Yang Direferensikan

Setiap kali instans jenis Java.Lang.Object atau subkelas dipindai selama GC, seluruh grafik objek yang dirujuk instans juga harus dipindai. Grafik objek adalah sekumpulan instans objek yang dirujuk oleh "instans akar" ditambah semuanya yang dirujuk oleh apa yang dirujuk oleh instans akar, secara rekursif.

Pertimbangkan kelas berikut:

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)));
    }
}

Ketika BadActivity dibangun, grafik objek akan berisi 10004 instans (1x BadActivity, 1x strings, 1x string[] dipegang oleh strings, instans string 10000x), yang semuanya perlu dipindai setiap kali BadActivity instans dipindai.

Hal ini dapat berdampak merugikan pada waktu pengumpulan Anda, yang mengakibatkan peningkatan waktu jeda GC.

Anda dapat membantu GC dengan mengurangi ukuran grafik objek yang di-root oleh instans serekan Pengguna. Dalam contoh di atas, ini dapat dilakukan dengan berpindah BadActivity.strings ke kelas terpisah yang tidak mewarisi dari 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)));
    }
}

Koleksi Kecil

Koleksi kecil dapat dilakukan secara manual dengan memanggil GC. Kumpulkan(0). Koleksi kecil murah (jika dibandingkan dengan koleksi utama), tetapi memiliki biaya tetap yang signifikan, sehingga Anda tidak ingin memicunya terlalu sering, dan harus memiliki waktu jeda beberapa milidetik.

Jika aplikasi Anda memiliki "siklus tugas" di mana hal yang sama dilakukan berulang-ulang, mungkin disarankan untuk melakukan koleksi kecil secara manual setelah siklus tugas berakhir. Contoh siklus tugas meliputi:

  • Siklus penyajian dari satu bingkai permainan.
  • Seluruh interaksi dengan dialog aplikasi tertentu (membuka, mengisi, menutup)
  • Sekelompok permintaan jaringan untuk menyegarkan/menyinkronkan data aplikasi.

Koleksi Utama

Koleksi utama dapat dilakukan secara manual dengan memanggil GC. Collect() atau GC.Collect(GC.MaxGeneration).

Mereka harus jarang dilakukan, dan mungkin memiliki waktu jeda detik pada perangkat bergaya Android saat mengumpulkan timbunan 512MB.

Koleksi utama hanya boleh dipanggil secara manual, jika pernah:

Diagnostik

Untuk melacak kapan referensi global dibuat dan dihancurkan, Anda dapat mengatur properti sistem debug.mono.log untuk berisi gref dan/atau gc.

Konfigurasi

Pengumpul sampah Xamarin.Android dapat dikonfigurasi dengan mengatur MONO_GC_PARAMS variabel lingkungan. Variabel lingkungan dapat diatur dengan tindakan Build AndroidEnvironment.

Variabel MONO_GC_PARAMS lingkungan adalah daftar parameter berikut yang dipisahkan koma:

  • nursery-size = ukuran : Mengatur ukuran pembibitan. Ukuran ditentukan dalam byte dan harus berupa kekuatan dua. Akhiran k , m dan g dapat digunakan untuk menentukan kilo-, mega- dan gigabyte, masing-masing. Pembibitan adalah generasi pertama (dari dua). Pembibitan yang lebih besar biasanya akan mempercepat program tetapi jelas akan menggunakan lebih banyak memori. Ukuran pembibitan default 512 kb.

  • soft-heap-limit = ukuran : Konsumsi memori terkelola maksimum target untuk aplikasi. Ketika penggunaan memori di bawah nilai yang ditentukan, GC dioptimalkan untuk waktu eksekusi (lebih sedikit koleksi). Di atas batas ini, GC dioptimalkan untuk penggunaan memori (lebih banyak koleksi).

  • evacuation-threshold = ambang : Menetapkan ambang evakuasi dalam persen. Nilai harus berupa bilangan bulat dalam rentang 0 hingga 100. Defaultnya adalah 66. Jika fase pembersihan koleksi menemukan bahwa hunian jenis blok tumpukan tertentu kurang dari persentase ini, ia akan melakukan koleksi penyalinan untuk jenis blok tersebut dalam koleksi utama berikutnya, sehingga memulihkan hunian mendekati 100 persen. Nilai 0 mematikan evakuasi.

  • bridge-implementation = implementasi bridge : Ini akan mengatur opsi GC Bridge untuk membantu mengatasi masalah performa GC. Ada tiga nilai yang mungkin: lama , baru , tarjan.

  • bridge-require-precise-merge: Jembatan Tarjan berisi pengoptimalan yang mungkin, pada kesempatan langka, menyebabkan objek dikumpulkan satu GC setelah pertama kali menjadi sampah. Termasuk opsi ini menonaktifkan pengoptimalan tersebut, membuat GC lebih dapat diprediksi tetapi berpotensi lebih lambat.

Misalnya, untuk mengonfigurasi GC agar memiliki batas ukuran timbunan 128MB, tambahkan file baru ke Proyek Anda dengan tindakanAndroidEnvironment Build dengan konten:

MONO_GC_PARAMS=soft-heap-limit=128m