Bellek sızıntısı (Memory Leak), uygulamanın artık ihtiyaç duyulmayan nesnelere referansları tutması ve çöp toplayıcının (GC) işgal edilen belleği serbest bırakmasını engellemesi durumudur. LeakCanary'ye göre, iyi yazılmış uygulamalarda bile her 10.000 satır kodda 3–5 sızıntı bulunur. Her sızıntı kullanılabilir belleği kademeli olarak azaltarak yavaşlamalara ve OutOfMemoryError'a yol açar.
Önemli Noktalar
Bellek sızıntısı (Memory Leak), bir nesnenin güçlü referans (Strong Reference) zinciri aracılığıyla erişilebilir durumda kalması, mantıksal olarak uygulama tarafından artık ihtiyaç duyulmamasına rağmen çöp toplayıcının (GC) bu nesneyi canlı olarak kabul etmesi ve işgal ettiği belleği serbest bırakmaması durumudur. Sonuç olarak, kullanılabilir yığın belleği sürekli azalır ve GC duraklamalarının sıklığı artar.
Manuel bellek yönetimine sahip dillerin (C, C++) aksine, Java/Kotlin'de sızıntı unutulmuş bir free() değil, unutulmuş bir referanstır. Bir GC Kökünden sızan nesneye kadar güçlü bir referans bulunduğu sürece, GC onu gerekli olarak kabul eder. Tipik GC Kökleri: statik alanlar, aktif iş parçacıkları, çağrı yığını, JNI genel referansları.
Sızıntıların tehlikesi birikimli etkileridir. 100 KB'lık bir sızıntı fark edilmez, ancak bu tür 100 sızıntı 10 MB yer kaplar ve uygulama sık GC nedeniyle yavaşlamaya başlar. Kritik sızıntı kütlesi OutOfMemoryError ve uygulama çökmesine yol açar. Sızıntı belirtileri: Profiler grafiğinde bellek tüketiminin sürekli artışı, STW (Stop The World) ile sık GC duraklamaları ve UI performansında düşüş.
Beş tür sızıntı, mobil geliştirmedeki vakaların %95'ini kapsar. Her birinin kendi nedeni ve karakteristik kod kalıbı vardır.
En bilinen Android sızıntısı, bir Activity veya Context'e statik referans depolamaktır. Tipik kod: onDestroy()'de null yapılmayan statik bir Activity alanı. Statik alan yaşadığı sürece, 1–10 MB yer kaplayabilen View ağacıyla birlikte tüm Activity yaşar. Bu, LeakCanary'nin ilk bulduğu klasik sızıntıdır.
Çözüm: Activity veya Context'i asla statik alanlarda saklamayın. Activity'den daha uzun yaşayan singleton'lar için Application Context kullanın. Bir Activity'ye referans gerekiyorsa, WeakReference<Activity> kullanın.
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
Anonim sınıflar ve statik olmayan iç sınıflar, örtük olarak kapsayıcı sınıfa bir referans tutar. onDestroy()'den sonra yürütülen bir Handler'a aktarılan Runnable, tüm Activity'yi tutar. Bir Activity'yi yakalayan Retrofit geri çağrısı da aynısını yapar. Bu en sinsi sızıntı türüdür — örtük referans kodda görünmez.
Kotlin object ifadeleri ve lambdalar da dış sınıfa referanslar yakalar. İç sınıfları statik (veya Kotlin'de üst düzey) yapın ve dış referansları WeakReference aracılığıyla iletin. Lambdalar için viewLifecycleOwner ile Lifecycle-aware bir yaklaşım kullanın.
Sistem hizmetlerine abone olup aboneliği kaldırmamak doğrudan bir sızıntıdır. onResume()'da kaydedilen ancak onPause()'da unregister çağırmayan SensorManager, LocationManager, NotificationListener Activity'yi tutar. Benzer şekilde: CompositeDisposable'a eklenmemiş RxJava Disposable ve GlobalScope aracılığıyla başlatılan bir coroutine.
Lifecycle-aware bileşenleri kullanın: LifecycleOwner ile observe() onDestroy()'da otomatik olarak aboneliği kaldırır. RxJava için — DisposableObserver ile viewLifecycleOwner.lifecycle.addObserver. Coroutine'ler için — lifecycleScope.launch() yaşam döngüsüne bağlanır.
// Lifecycle aracılığıyla otomatik abonelik iptali
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// lifecycleScope ile coroutine'ler
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Bitmap, önemli miktarda yığın belleği kaplar: bir FullHD bitmap 1920 × 1080 × 4 bayt = 8,3 MB'dir. Liste öğesi başına bir Bitmap oluşturulur ve gizleme sırasında recycle() çağrılmazsa, bellek hızla tükenir. Eski Android sürümlerinde (3.0 öncesi) Bitmap yerel bellekte depolanıyordu, ancak modern sürümlerde Dalvik/ART yığınındadır ve GC onu yalnızca güçlü referans yoksa serbest bırakabilir.
Görüntü yüklemek için Glide veya Coil kullanın — bu kitaplıklar önbelleğe alma ve geri dönüşümü otomatik olarak yönetir. Doğrudan Bitmap ile çalışıyorsanız, artık görüntülenmeyen büyük görüntüler için bitmap.recycle() çağırın ve küçültülmüş kopyalar yüklemek için inSampleSize kullanın.
Fragment'in iki yaşam döngüsü vardır: Fragment'in kendisi ve View'ininki. onDestroyView()'den sonra View ağacı yok edilir, ancak harici bir referans varsa Fragment'in kendisi bellekte kalabilir. Tipik bir hata, ViewPager bağdaştırıcısında veya yok edildiğinde temizlenmeyen bir navigasyon grafında Fragment'e referans depolamaktır.
Uzun ömürlü nesnelerin alanlarında Fragment'e referans asla saklamayın. İç içe fragment'ler için childFragmentManager'ı ve aralarında veri aktarımı için LifecycleOwner ile observe()'u kullanın. ViewPager2 bu sorunu API düzeyinde çözdü: FragmentTransactionAdapter yaşam döngüsünü doğru şekilde yönetir.
Sızıntı tespiti iki gerçeğin doğrulanmasını gerektirir: beklenen yaşam süresinden sonra belleğin geri dönmemesi ve belirli bir türdeki nesne sayısının azalmadan artması. Tanı süreci üç aşamayı içerir.
İlk aşama — Android Studio'da Memory Profiler aracılığıyla görsel kontrol. Memory sekmesini açın, hedef eylemi gerçekleştirin (ekranı açıp kapatın), GC'ye (Garbage Collection) basın ve belleğin başlangıç seviyesine dönüp dönmediğini görün. 3–4 açma-kapama döngüsünden sonra bellek sürekli artıyorsa — sızıntı vardır.
İkinci aşama — Heap Dump alma. Memory Profiler'da Dump Java Heap'e basın. Ortaya çıkan .hprof dosyasını Android Studio'da açın: yığındaki tüm nesneleri boyutları ve referanslarıyla göreceksiniz. Ekranı kapattıktan sonra sayısı sıfır olması gereken sınıfları arayın. Örneğin, kapattıktan sonra sayısı 2 olan MainActivity — açık bir sızıntıdır.
Üçüncü aşama — Retained Size ve GC Root analizi. Android Studio'da Retained Size'ı analiz edin: bu nesneyi kaldırırsanız ne kadar bellek serbest kalır? GC Root'tan nesneye giden yol, onu neyin tuttuğunu gösterir: Static field → HashMap → Activity — ve sızıntı noktasını görürsünüz. Reference widget paneli, nesnenin tüm sahiplerini gösterir.
Dört araç, otomatik tespitten derin Heap Dump analizine kadar sızıntı aramayı kapsar.
| Araç | Yöntem | Sonuç biçimi |
|---|---|---|
| LeakCanary | Otomatik izleme | Heap Dump + sızıntı stack trace |
| Android Memory Profiler | Manuel izleme | Bellek grafiği + Heap Dump |
| MAT (Eclipse) | Derin analiz | Dominator Tree raporu + GC Root yolu |
| Perfetto | Sistem çapında izleme | Zaman çizelgesi + yerel bellek |
LeakCanary herhangi bir Android projesi için olmazsa olmazdır. Activity/Fragment yaşam döngüsünün sonunda sızıntıları otomatik olarak algılar ve stack trace ile tam sızıntı konumunu gösterir. Entegrasyon: build.gradle'da bir satır. LeakCanary 2.x manuel başlatma gerektirmez — Application Watcher'ı otomatik olarak kaydeder.
Sızıntıların önlenmesi, her aşamada kodu kontrol eden bir dizi kural ve araç aracılığıyla geliştirme sürecine yerleştirilir.
Bir Activity, Fragment veya View'e referansı asla statik alanda, singleton'da veya uzun ömürlü bir nesnede saklamayın. Referans kaçınılmazsa, WeakReference kullanın veya verileri ViewModel aracılığıyla saklayın; ViewModel tam gerektiği kadar yaşar ve bir View'i doğrudan tutmaz.
Android Architecture Components'ten ViewModel ve LiveData, yaşam döngüsü sorununu mimari düzeyde çözer. ViewModel ekran dönüşünden kurtulur ve View referansları içermez. LiveData, onDestroy()'da otomatik olarak gözlemcinin aboneliğini kaldırır. Sistem hizmetlerine manuel abonelik yerine bunları kullanın.
Kod incelemesinde şunlara dikkat edin: Context/View türlerinde statik alanlar, anonim sınıflar, Activity yakalayan lambdalar, manuel abonelikler, composite olmadan RxJava disposable, Bundle aracılığıyla Fragment saklama. Kotlin'de ayrıca yaşam döngüsü bağlaması olmadan launch kullanan coroutine'leri kontrol edin.
LeakCanary test hattının bir parçası olarak çalışabilir: LeakCanary ile kabul testlerini çalıştırın ve bir sızıntı bulunursa derlemeyi başarısız yapın. Bu, sızıntıların üretime ulaşmasını engeller. Android Lint'in StaticFieldLeak kuralı ile kontrolü tamamlayın — statik analiz düzeyinde potansiyel sızıntıları bulur.
// Testlerde LeakCanary
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // sızıntı varsa başarısız
}
}
Sıkça sorulan sorular
Sızıntı nedendir, OutOfMemoryError ise sonuçtur. Bir sızıntı OOM'ye yol açmaz, ancak düzinelerce sızıntının birikmesi Heap'i tüketir. OOM ölümcül bir istisnadır, sızıntı ise zamanla ona götüren bir kalıptır.
Android Memory Profiler aracılığıyla: ekranı 5 kez açıp kapatın, her kapatıştan sonra GC'yi çağırın. Bellek temel seviyeye dönmezse — sızıntı vardır. Bir Heap Dump alın ve kapattıktan sonra sayısı 0'dan büyük olan Activity sınıfını listede bulun.
Kısmen. Kotlin null-safety sorununu çözer ancak güçlü referansları yönetmez. lifecycleScope ve viewModelScope ile coroutine'ler arka plan görevlerinden kaynaklanan sızıntıları önlerken, sealed class ve data class sızıntılara yol açan durumların sayısını azaltır. Ana koruma dil özellikleri değil, mimari kalıplardır.
LeakCanary bazen yanlış pozitifler verir: bir nesne geçici olarak sistem tarafından tutulabilir (örneğin, InputMethodManager son View'i tutar). Manuel olarak kontrol edin: Retained Size < 1 KB ise ve GC Root bir sistem hizmetiyse, büyük olasılıkla yanlış pozitiftir.
Hayır. Sızıntılar GC'li herhangi bir platformda mümkündür: iOS (Swift/Objective-C), Flutter (Dart), web tarayıcıları (JavaScript). Mekanizmalar aynıdır — GC Root'tan güçlü referans. iOS'te ARC belleği otomatik olarak yönetir, ancak nesneler arasındaki retain cycle aynı sızıntıyı oluşturur.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun