Kebocoran memori (memory leak) — situasi ketika aplikasi tidak membebaskan memori yang ditempati oleh objek yang tidak lagi diperlukan. Dalam pengembangan mobile, hal ini sangat kritis: heap terbatas dan tidak adanya swap menyebabkan OutOfMemoryError dan crash aplikasi. Menurut Purdue University (2022), 35% aplikasi Android di Google Play mengandung setidaknya satu kebocoran memori. Mari kita bahas skenario tipikal, alat diagnostik, dan metode perbaikan.
Poin utama
Kebocoran memori — adalah situasi ketika memori yang dialokasikan tidak dikembalikan ke sistem setelah objek tidak lagi diperlukan oleh program. Garbage collector menganggap objek tersebut hidup karena ada rantai referensi aktif dari GC Root ke objek tersebut.
Di Java/Kotlin, garbage collector bekerja secara otomatis, tetapi tidak dapat menentukan bahwa suatu objek secara logis tidak diperlukan jika ada referensi teknis ke objek tersebut. Pengembang harus secara eksplisit memutus koneksi yang tidak perlu. Di Swift/Objective-C, ARC secara otomatis menghitung referensi, tetapi retain cycles memblokir penghitungan ulang ke nol.
Bahaya utama kebocoran — efek kumulatif. Setiap kebocoran mengonsumsi sejumlah kecil memori, tetapi dengan banyak perpindahan antar layar (rotasi layar, membuka/menutup Activity), kebocoran menumpuk hingga batas heap habis.
Kebocoran — objek tidak dapat diakses oleh kode, tetapi tidak dihapus oleh GC. Pembengkakan — objek secara logis diperlukan, tetapi disimpan dalam jumlah berlebihan. Contoh pembengkakan: cache gambar 100 MB dengan set kerja 30 MB. Kedua masalah menyebabkan OOM, tetapi penyebab dan metode penanganannya berbeda.
ART (Android Runtime) menggunakan garbage collection generasional dengan concurrent compaction. Memori dibagi menjadi generasi muda (Young), tua (Old), dan objek besar (Large). Objek yang telah bertahan beberapa siklus GC dipindahkan ke Old generation, di mana pengumpulan lebih jarang terjadi — ini mempercepat siklus biasa.
GC dimulai ketika heap mencapai ambang okupansi tertentu (biasanya 75-85%). Selama GC, semua thread aplikasi dijeda (STW — Stop The World). Semakin banyak objek hidup, semakin lama jeda. Kebocoran meningkatkan jumlah objek hidup, memperpanjang jeda GC.
Pengumpul menentukan objek hidup dengan melintasi graf dari GC Roots: field statis, variabel stack dari thread aktif, referensi JNI. Objek apa pun yang dapat dijangkau melalui referensi dari akar-akar ini dianggap hidup — bahkan jika pengembang tahu bahwa objek itu tidak lagi diperlukan.
// Contoh: koleksi statis sebagai GC Root — kebocoran permanen
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference tidak memblokir GC — perilaku yang benar
}
}
WeakReference memecahkan masalah: GC mengabaikan weak reference saat menentukan objek hidup. Jika hanya weak reference yang tersisa ke suatu objek, objek tersebut akan dikumpulkan dalam siklus GC berikutnya.
Activity Context — skenario kebocoran paling masif di Android. Jika singleton, field statis, atau layanan berumur panjang menyimpan referensi ke Activity Context, seluruh Activity dengan semua View tidak dapat dikumpulkan oleh GC. Solusi: gunakan Application Context untuk objek berumur panjang.
Handler dan pesan yang dikirim — Handler.postDelayed(runnable, delay) menempatkan pesan dalam antrian Main Looper. Jika Activity dihancurkan sebelum penundaan berakhir, pesan masih dalam antrian dan menyimpan referensi melalui Runnable → kelas anonim → kelas eksternal (Activity).
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // wajib: bersihkan antrian
super.onPause()
}
}
Inner Classes — kelas dalam non-statis memiliki referensi implisit ke instance kelas luar. Jika kelas luar adalah Activity dan kelas dalam diteruskan ke suatu tempat di luar (misalnya, ke RecyclerView.Adapter), Activity tidak dapat dikumpulkan.
Android Studio Memory Profiler — alat bawaan untuk memantau heap secara real-time. Menampilkan grafik memori yang terpakai, jumlah alokasi dan objek berdasarkan jenis. Memungkinkan merekam heap dump dan mengekspor ke format HPROF untuk analisis di MAT.
Eclipse MAT (Memory Analyzer Tool) — analisator heap dump desktop. Secara otomatis membuat Leak Suspects Report yang menyoroti objek dengan retained size terbesar dan menyarankan rantai GC root yang mungkin untuk setiap objek yang mencurigakan.
Xcode Memory Graph Debugger — untuk iOS. Menjeda aplikasi dan memvisualisasikan graf objek. Retain cycles disorot dengan warna merah, Anda dapat mengklik objek apa pun untuk melihat retain count dan referensinya.
| Alat | Kemampuan | Kompleksitas |
|---|---|---|
| Memory Profiler | Grafik real-time, heap dump, pelacakan alokasi objek | Rendah |
| Eclipse MAT | Pohon dominator, tersangka kebocoran, kueri OQL | Sedang |
| LeakCanary | Deteksi otomatis, jejak kebocoran dalam notifikasi | Minimal |
| Xcode Memory Graph | Graf visual retain cycles, daftar objek hidup | Rendah |
Menurut Uber Engineering Blog, penerapan profiling memori otomatis (LeakCanary + analisis heap dump) dalam pipeline CI/CD mengurangi jumlah insiden terkait memori di produksi sebesar 60% dalam 3 bulan.
Mengganti Context — jika objek hidup lebih lama dari Activity, gunakan applicationContext. Semua objek berumur panjang (singleton, repositori, database helpers) harus menerima Application Context, bukan Activity Context. Pengecualian: komponen UI yang membutuhkan akses ke tema atau resource spesifik Activity.
Komponen Lifecycle-aware — penggunaan LifecycleObserver, DefaultLifecycleObserver, atau reactivex secara otomatis membatalkan langganan saat onDestroy. Android Jetpack menyediakan lifecycleScope dan viewModelScope, yang dibersihkan oleh event yang sesuai.
Static inner class — jika kelas dalam tidak memerlukan akses ke field kelas luar, buatlah static. Kelas dalam statis tidak memiliki referensi implisit ke kelas luar. Jika akses diperlukan, gunakan WeakReference untuk referensi eksplisit.
class MyActivity : AppCompatActivity() {
// ❌ Kelas dalam non-statis — referensi implisit ke MyActivity
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ Kelas dalam statis — tanpa referensi implisit
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
Di iOS, gunakan capture lists: [weak self] dalam closure yang dapat hidup lebih lama dari pembuatnya. Untuk delegasi, gunakan referensi lemah (weak var delegate). Untuk closure yang dijamin hanya dipanggil selama hidup self, dapat menggunakan [unowned self], tetapi hati-hati — mengakses objek yang telah dibebaskan akan menyebabkan crash.
Pertanyaan yang sering diajukan
Di Android, lakukan beberapa kali perpindahan antar layar (Activity A → B → A → B) dan periksa adb shell dumpsys meminfo package_name. Jika Total PSS terus meningkat dan tidak kembali ke nilai awal — ada kebocoran. Di iOS serupa: gunakan Debug Memory Graph di Xcode untuk pemeriksaan visual.
Ya, jika CoroutineScope tidak dibatalkan saat komponen dihancurkan. Coroutine yang dijalankan di GlobalScope terus berjalan bahkan setelah finish() Activity. Solusi: gunakan viewModelScope (dibatalkan di onCleared) atau lifecycleScope (dibatalkan di onDestroy). Untuk Scope kustom, buat lifecycle-aware scopes melalui LifecycleOwner.
Bitmap menyimpan data piksel di native heap, bukan di Java heap. Ini berarti Java GC tidak melihat ukuran sebenarnya Bitmap. Jika Bitmap tidak dipanggil recycle() atau referensi tidak di-null-kan, memori native tidak akan dibebaskan. Gunakan BitmapFactory dengan inSampleSize untuk memuat saluran yang diperkecil dan Glide/Coil untuk manajemen cache otomatis.
Field statis — adalah GC Root. Ia hidup selama kelas dimuat (di Android — selama Process hidup). Jika field statis mereferensikan Activity, Bitmap, View, atau objek berat lainnya, objek tersebut tidak akan pernah dikumpulkan oleh GC. Field statis — referensi abadi. Solusi: simpan hanya WeakReference atau null-kan field statis di onDestroy.
ARC secara otomatis membebaskan objek ketika penghitung referensi kuat turun ke nol. Retain cycle — satu-satunya cara kebocoran di ARC. Selalu gunakan weak untuk referensi parent→child, di mana child harus hidup lebih lama dari parent (delegasi, data source). Untuk closure, gunakan capture list [weak self] dan periksa self untuk nil di dalam closure.
Kesimpulan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga