Mobil uygulamalarda bellek sızıntısı — nedir, nedenleri ve tespit yöntemleri

Yazar: IT Sectr Yayınlanma: 2026-03-29 Okuma süresi: 9 dk

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

  • Memory Leak — uygulama mantığında ona aktif referans olmamasına rağmen bir nesnenin bellekte kalması
  • Activity veya Context'e statik referanslar — Android'de sızıntıların en yaygın nedeni
  • LeakCanary — Android'de otomatik sızıntı tespiti için standart araç
  • WeakReference ve Application Context — sızıntıları önlemenin temel teknikleri
  • Lifecycle-aware bileşenler aboneliklerle ilgili tüm sızıntı sınıfını ortadan kaldırır

Bellek sızıntısı nedir

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üşüş.

Mobil uygulamalarda yaygın sızıntı türleri

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.

Activity veya Context'e statik referans

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.

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

Örtük referansa sahip iç sınıflar

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.

Kaldırılmamış dinleyiciler ve abonelikler

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.

kotlin
// 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) }
}

recycle olmadan Bitmap

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.

onDestroyView'den sonra Fragment referansı

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.

Bellek sızıntısı nasıl tespit edilir

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.

Sızıntı tespit araçları

Dört araç, otomatik tespitten derin Heap Dump analizine kadar sızıntı aramayı kapsar.

AraçYöntemSonuç biçimi
LeakCanaryOtomatik izlemeHeap Dump + sızıntı stack trace
Android Memory ProfilerManuel izlemeBellek grafiği + Heap Dump
MAT (Eclipse)Derin analizDominator Tree raporu + GC Root yolu
PerfettoSistem çapında izlemeZaman ç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.

Bellek sızıntıları nasıl önlenir

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.

Güçlü referans kuralı

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.

Lifecycle-aware mimari

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.

GC Root odaklı kod incelemesi

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.

CI'da otomatik kontrol

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.

kotlin
// 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

Bellek sızıntısı OutOfMemoryError'dan nasıl farklıdır?

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.

LeakCanary olmadan sızıntı nasıl bulunur?

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.

Kotlin dil düzeyinde sızıntıları önleyebilir mi?

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 neden olmayan bir sızıntı bulur?

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.

Bellek sızıntıları yalnızca Android'de mi olur?

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

  • Memory Leak — unutulmuş bir güçlü referans nedeniyle GC'nin serbest bırakamadığı nesne
  • Activity ve Context'e statik referanslar — sızıntıların en yaygın nedeni
  • Örtük referanslar (anonim sınıflar, lambdalar ve RxJava abonelikleri) açık olanlardan daha sinsi
  • LeakCanary sızıntıları otomatik olarak bulur ve tam stack trace gösterir
  • Lifecycle-aware bileşenler (ViewModel, LiveData, lifecycleScope) bir sızıntı sınıfını ortadan kaldırır
  • Heap Dump ve Retained Size analizi — manuel tanının ana yöntemi
  • Önleme güçlü referanslara odaklı kod incelemesi ve LeakCanary ile CI kontrolünü içerir

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.

Projeyi tartış

Ayrıca okuyun