Цурење меморије: шта је то, типични сценарији и дијагностика

Аутор: IT Sectr Објављено: 2026-07-29 Време читања: 10 мин

Цурење меморије (memory leak) — ситуација када апликација не ослобађа меморију заузету објектима који више нису потребни. У мобилном развоју ово је посебно критично: ограничени хеап и одсуство свап-а доводе до OutOfMemoryError и пада апликације. Према подацима Purdue University (2022), 35% Android апликација у Google Play-у садржи бар једно цурење меморије. Размотрићемо типичне сценарије, алате за дијагностику и методе отклањања.

Главне ствари

  • GC Root — улазна тачка кроз коју сакупљач смећа одређује живе објекте
  • Цурење Context — прослеђивање Activity Context-а синглтону доводи до задржавања целе View хијерархије
  • Handler са postDelayed — ако је Activity уништено, Handler му не дозвољава да оде на GC
  • Heap dump — основни метод анализе цурења кроз MAT или Android Profiler
  • SoftReference — алтернатива WeakReference-у за кешеве са аутоматским чишћењем при несташици меморије

Шта је цурење меморије у мобилним апликацијама?

Цурење меморије — то је ситуација када се додељена меморија не враћа систему након што објекат престане да буде потребан програму. Сакупљач смећа такав објекат сматра живим, јер до њега води активан ланац референци од GC Root-а.

У Java/Kotlin-у сакупљач смећа ради аутоматски, али не може да одреди да објекат логички није потребан ако постоји техничка референца на њега. Програмер мора експлицитно да прекине непотребне везе. У Swift/Objective-C-у ARC аутоматски броји референце, али retain cycles блокирају нулирање бројача.

Главна опасност цурења — кумулативни ефекат. Свако цурење троши малу количину меморије, али при вишеструким прелазима између екрана (ротацијама екрана, отварању/затварању Activity) цурења се акумулирају док не исцрпе лимит хеап-а.

Чиме се цурење разликује од надимања?

Цурење — објекат је недоступан коду, али га GC није уклонио. Надимање — објекат је логички потребан, али се чува у прекомерној количини. Пример надимања: кеш слика од 100 MB са радним сетом од 30 MB. Оба проблема воде ка OOM-у, али узроци и методе лечења су различити.

Како ради сакупљач смећа и зашто настају цурења?

ART (Android Runtime) користи генерацијско сакупљање смећа са concurrent compaction. Меморија се дели на младу генерацију (Young), стару (Old) и огромне објекте (Large). Објекти који су преживели неколико GC циклуса премештају се у Old generation, где се сакупљање ређе дешава — то убрзава уобичајене циклусе.

GC почиње када хеап достигне одређени праг заузетости (обично 75-85%). Током GC-ја све нити апликације се заустављају (STW — Stop The World). Што је више живих објеката, пауза је дужа. Цурења повећавају број живих објеката, продужавајући GC паузе.

Сакупљач одређује живе објекте обилазећи граф од GC Roots: статичка поља, стек променљиве активних нити, JNI референце. Сваки објекат достижан преко референци од ових корена сматра се живим — чак и ако програмер зна да више није потребан.

kotlin
// Пример: статичка колекција као GC Root — трајно цурење
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 не блокира GC — исправно понашање
    }
}

WeakReference решава проблем: GC игнорише слабе референце при одређивању живих објеката. Ако су на објекат остале само слабе референце, биће сакупљен у следећем GC циклусу.

Типични сценарији цурења у Android и iOS

Activity Context — најмасовнији сценариј цурења у Android-у. Ако синглтон, статичко поље или дуготрајни сервис чува референцу на Activity Context, цело Activity са свим View-овима не може да сакупи GC. Решење: користите Application Context за дуготрајне објекте.

Handler и послате поруке — Handler.postDelayed(runnable, delay) поставља поруку у ред Main Looper-а. Ако је Activity уништено пре истека кашњења, порука је још увек у реду и држи референцу кроз Runnable → анонимну класу → спољашњу класу (Activity).

kotlin
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) // обавезно: очисти ред
        super.onPause()
    }
}

Inner Classes — нестатичка унутрашња класа има имплицитну референцу на инстанцу спољашње класе. Ако је спољашња класа Activity, а унутрашња класа је прослеђена негде напоље (нпр. у RecyclerView.Adapter), Activity не може да се сакупи.

  • TimerTask и ScheduledExecutorService — задаци планирани пре уништења Activity
  • BroadcastReceiver — нерегистрован у onPause/onDestroy наставља да држи Context
  • ViewModel са референцом на View — ViewModel надживљава Activity, референца на View води ка цурењу
  • Retrofit Call — ако Call није отказан, одговор стиже у уништени Fragment

Алати за дијагностику цурења меморије

Android Studio Memory Profiler — уграђени алат за праћење хеап-а у реалном времену. Приказује график заузете меморије, број алокација и објеката по типовима. Омогућава снимање heap dump-а и извоз у HPROF формат за анализу у MAT-у.

Eclipse MAT (Memory Analyzer Tool) — десктоп анализатор heap dump-а. Аутоматски прави Leak Suspects Report, који истиче објекте са највећим retained size-ом и предлаже могући GC root ланац за сваки сумњиви објекат.

Xcode Memory Graph Debugger — за iOS. Зауставља апликацију и визуализује граф објеката. Retain cycles су истакнути црвеном бојом, може се кликнути на било који објекат и видети његов retain count и референце.

АлатМогућностиСложеност
Memory ProfilerГрафик у реалном времену, heap dump, праћење алокације објекатаНиска
Eclipse MATDominator tree, Leak Suspects, OQL упитиСредња
LeakCanaryАутоматско откривање, траг цурења у обавештењуМинимална
Xcode Memory GraphВизуелни граф retain cycles, листа живих објекатаНиска

Према Uber Engineering Blog-у, увођење аутоматског профилисања меморије (LeakCanary + анализа heap dump-а) у CI/CD цевовод смањује број инцидената везаних за меморију у продукцији за 60% у року од 3 месеца.

Методе отклањања цурења

Замена Context — ако објекат живи дуже од Activity, користите applicationContext. Сви дуготрајни објекти (синглтони, репозиторијуми, database helpers) треба да добију Application Context, а не Activity Context. Изузетак: UI компоненте којима је потребан приступ теми или ресурсима специфичним за Activity.

Lifecycle-aware компоненте — коришћење LifecycleObserver, DefaultLifecycleObserver или reactivex аутоматски отказује претплате при onDestroy. Android Jetpack пружа lifecycleScope и viewModelScope, који се чисте одговарајућим догађајем.

Static inner class — ако унутрашњој класи није потребан приступ пољима спољашње, учините је static. Статичка унутрашња класа нема имплицитну референцу на спољашњу класу. Ако је приступ потребан, користите WeakReference за експлицитну референцу.

kotlin
class MyActivity : AppCompatActivity() {

    // ❌ Нестатичка унутрашња класа — имплицитна референца на MyActivity
    inner class BadListener : SomeListener {
        override fun onEvent() { /*...*/ }
    }

    // ✅ Статичка унутрашња класа — без имплицитне референце
    class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
        override fun onEvent() { /*...*/ }
    }
}

У iOS-у користите capture lists: [weak self] у затворењима која могу надживети творца. За делегате користите слабе референце (weak var delegate). За затворења која су гарантовано позвана само током живота self-а, може се користити [unowned self], али опрезно — приступање ослобођеном објекту узроковаће crash.

Често постављана питања

Како пронаћи цурење без посебних алата?

У Android-у извршите неколико прелаза између екрана (Activity A → B → A → B) и проверите adb shell dumpsys meminfo package_name. Ако Total PSS стабилно расте и не враћа се на почетну вредност — постоји цурење. У iOS-у слично: користите Debug Memory Graph у Xcode-у за визуелну проверу.

Може ли Kotlin корутина изазвати цурење?

Да, ако CoroutineScope није отказан при уништењу компоненте. Корутина покренута у GlobalScope наставља да се извршава чак и након finish() Activity. Решење: користите viewModelScope (отказује се у onCleared) или lifecycleScope (отказује се у onDestroy). За сопствене Scope-ове креирајте lifecycle-aware scopes кроз LifecycleOwner.

Како Bitmap утиче на цурења?

Bitmap чува пикселне податке у native heap-у, а не у Java heap-у. То значи да Java GC не види стварну величину Bitmap-а. Ако се на Bitmap-у не позове recycle() или се референца не нулира, native меморија се неће ослободити. Користите BitmapFactory са inSampleSize за учитавање умањених копија и Glide/Coil за аутоматско управљање кешом.

Шта је цурење кроз статичко поље?

Статичко поље — то је GC Root. Живи док је класа учитана (у Android-у — док живи Process). Ако статичко поље референцира Activity, Bitmap, View или било који други тежак објекат, тај објекат никада неће бити сакупљен од стране GC-ја. Статичко поље — вечна референца. Решење: чувајте само WeakReference или нулирајте статичко поље у onDestroy.

Како избећи цурења у iOS-у са ARC?

ARC аутоматски ослобађа објекте када бројач јаких референци падне на нулу. Retain cycle — једини начин цурења при ARC-у. Увек користите weak за parent→child референце, где child треба да надживи parent (делегати, data source). За затворења користите capture list [weak self] и проверавајте self на nil унутар затворења.

Закључак

  • Цурење меморије — објекат је недоступан коду, али га GC није уклонио јер постоји активна референца од GC Root-а
  • GC Roots укључују статичка поља, стек променљиве и JNI референце; сваки објекат достижан од њих је жив
  • Цурење Context — најмасовнији проблем у Android-у: прослеђивање Activity Context-а синглтону или статичком пољу
  • Handler и Inner Class — други по учесталости узрок: неотказане поруке у Looper реду држе референцу на Activity
  • LeakCanary — стандардни алат за аутоматско откривање; ради heap dump и показује тачан GC root ланац
  • lifecycleScope и viewModelScope решавају проблем цурења кроз корутине — аутоматско отказивање при destroy
  • Профилишите меморију у CI/CD: LeakCanary у debug + анализа heap dump-а у тестном прогону треба да блокирају merge при новим цурењима

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође