Wyciek pamięci w aplikacjach mobilnych — co to jest, przyczyny i metody wykrywania

Autor: IT Sectr Opublikowano: 2026-03-29 Czas czytania: 9 min

Wyciek pamięci (Memory Leak) — sytuacja, gdy aplikacja przechowuje referencje do obiektów, które nie są już potrzebne, uniemożliwiając garbage collectora zwolnienie occupied memory. Według danych LeakCanary, nawet w dobrze napisanych aplikacjach występują 3–5 wycieków na 10 000 linii kodu. Każdy wyciek stopniowo zmniejsza dostępną pamięć, prowadząc do spowolnień i OutOfMemoryError.

Najważniejsze

  • Memory Leak — obiekt pozostaje w pamięci, mimo że nie ma do niego aktywnych referencji z logiki aplikacji
  • Statyczne referencje do Activity lub Context — najczęstsza przyczyna wycieków w Androidzie
  • LeakCanary — standardowe narzędzie do automatycznego wykrywania wycieków w Androidzie
  • WeakReference i Application Context — podstawowe techniki zapobiegania wyciekom
  • Komponenty Lifecycle-aware eliminują całą klasę wycieków związanych z subskrypcjami

Co to jest wyciek pamięci

Wyciek pamięci (Memory Leak) — to sytuacja, w której obiekt pozostaje osiągalny poprzez łańcuch silnych referencji (Strong Reference), choć logicznie nie jest już potrzebny aplikacji. Garbage collector (GC) uznaje taki obiekt za żywy i nie zwalnia zajmowanej przez niego pamięci. W rezultacie dostępna pamięć sterty (Heap) stale się zmniejsza, a częstotliwość pauz GC rośnie.

W przeciwieństwie do języków z ręcznym zarządzaniem pamięcią (C, C++), w Java/Kotlin wyciek to nie zapomniany free(), a zapomniana referencja. Dopóki istnieje strong reference od obiektu głównego (GC Root) do wyciekającego obiektu, GC uznaje go za potrzebny. Typowe GC Root: pola statyczne, aktywne wątki, stos wywołań, referencje JNI-globalne.

Zagrożenie wycieków — ich efekt kumulacyjny. Jeden wyciek 100 KB jest niezauważalny, ale 100 takich wycieków zajmuje 10 MB, a aplikacja zaczyna zwalniać z powodu częstych GC. Krytyczna masa wycieków prowadzi do OutOfMemoryError i crasha aplikacji. Objawy wycieku: stały wzrost zużycia pamięci na wykresie Profilera, częste pauzy GC z STW (Stop The World) i spadek wydajności UI.

Typowe rodzaje wycieków w aplikacjach mobilnych

Pięć rodzajów wycieków pokrywa 95% przypadków w programowaniu mobilnym. Każdy ma swoją przyczynę i charakterystyczny wzorzec w kodzie.

Statyczna referencja do Activity lub Context

Najbardziej znany wyciek w Androidzie — przechowywanie statycznej referencji do Activity lub Context. Typowy kod: statyczne pole Activity, które nie jest zerowane przy onDestroy(). Dopóki żyje pole statyczne — żyje całe Activity ze swoim drzewem View, które może zajmować 1–10 MB. To klasyczny wyciek, który LeakCanary znajduje w pierwszej kolejności.

Rozwiązanie: nigdy nie przechowuj Activity lub Context w polach statycznych. Używaj Application Context dla singletonów, które żyją dłużej niż Activity. Jeśli potrzebujesz referencji do Activity — użyj WeakReference<Activity>.

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

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

Klasy wewnętrzne z niejawną referencją

Anonimowe klasy i niestatyczne klasy zagnieżdżone niejawnie przechowują referencję do klasy zawierającej. Runnable przekazywany do Handlera, który wykonuje się po onDestroy(), przechowuje całe Activity. Callback Retrofit zamykający Activity robi to samo. To najbardziej podstępny typ wycieku — niejawna referencja nie jest widoczna w kodzie.

Kotlin object-wyrażenia i lambdy również przechwytują referencje do klasy zewnętrznej. Twórz klasy zagnieżdżone jako statyczne (lub top-level w Kotlin) i przekazuj referencje zewnętrzne przez WeakReference. Dla lambd używaj podejścia Lifecycle-aware z viewLifecycleOwner.

Niewypisane słuchacze i subskrypcje

Subskrypcja usług systemowych bez wypisywania się — bezpośredni wyciek. SensorManager, LocationManager, NotificationListener zarejestrowane w onResume() bez wywołania unregister w onPause() przechowują Activity. Analogicznie: RxJava Disposable niedodany do CompositeDisposable i coroutine uruchomiona przez GlobalScope.

Używaj Lifecycle-aware komponentów: observe() z LifecycleOwner automatycznie wypisuje się przy onDestroy(). Dla RxJava — viewLifecycleOwner.lifecycle.addObserver z DisposableObserver. Dla coroutines — lifecycleScope.launch() jest powiązany z cyklem życia.

kotlin
// automatyczne wypisywanie się przez Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// coroutines z lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap bez recycle

Bitmap zajmuje znaczną ilość pamięci sterty: jeden FullHD-bitmap — 1920 × 1080 × 4 bajty = 8.3 MB. Jeśli Bitmap jest tworzony dla każdego elementu listy i nie wywołuje się recycle() przy ukrywaniu, pamięć szybko się wyczerpuje. Na starych wersjach Androida (przed 3.0) Bitmap był przechowywany w pamięci native, ale na współczesnych — na stercie Dalvik/ART, a GC może go zwolnić tylko jeśli nie ma strong reference.

Używaj Glide lub Coil do ładowania obrazów — te biblioteki zarządzają buforowaniem i recycle automatycznie. Jeśli pracujesz bezpośrednio z Bitmap, wywołuj bitmap.recycle() dla dużych obrazów, które nie są już wyświetlane, i używaj inSampleSize do ładowania zmniejszonych kopii.

Fragment Reference po onDestroyView

Fragment ma dwa cykle życia: samego Fragment i jego View. Po onDestroyView() drzewo View jest niszczone, ale sam Fragment może pozostać w pamięci, jeśli istnieje zewnętrzna referencja. Typowy błąd — przechowywanie referencji do Fragment w adapterze ViewPager lub w grafie nawigacji, która nie jest czyszczona przy zniszczeniu.

Nigdy nie przechowuj referencji do Fragment w polach długożyjących obiektów. Używaj childFragmentManager dla zagnieżdżonych fragmentów i observe() z LifecycleOwner do przesyłania danych między nimi. ViewPager2 rozwiązał ten problem na poziomie API: FragmentTransactionAdapter poprawnie zarządza cyklem życia.

Jak wykryć wyciek pamięci

Wykrycie wycieku wymaga sprawdzenia dwóch faktów: pamięć nie wraca po expected lifetime i liczba obiektów określonego typu rośnie bez zmniejszania. Proces diagnostyki obejmuje trzy etapy.

Pierwszy etap — wizualne sprawdzenie przez Memory Profiler w Android Studio. Otwórz zakładkę Memory, wykonaj docelową akcję (otwórz i zamknij ekran), naciśnij GC (Garbage Collection) i sprawdź, czy pamięć wróciła do początkowego poziomu. Jeśli po 3–4 cyklach otwierania-zamykania pamięć stale rośnie — jest wyciek.

Drugi etap — wykonanie Heap Dump. W Memory Profiler naciśnij Dump Java Heap. Uzyskany plik .hprof otwórz w Android Studio: zobaczysz wszystkie obiekty na stercie z rozmiarami i referencjami. Szukaj klas, których liczba powinna być zerowa po zamknięciu ekranu. Na przykład MainActivity z liczbą 2 po zamknięciu — wyraźny wyciek.

Trzeci etap — analiza Retained Size i GC Root. W Android Studio analizuj Retained Size: ile pamięci zostanie zwolnione, jeśli usuniesz ten obiekt. Ścieżka od GC Root do obiektu pokazuje, co go utrzymuje: Static field → HashMap → Activity — i widzisz punkt wycieku. Panel Reference widget pokazuje wszystkich posiadaczy obiektu.

Narzędzia do wykrywania wycieków

Cztery narzędzia pokrywają wykrywanie wycieków od automatycznego wykrywania do głębokiej analizy Heap Dump.

NarzędzieMetodaFormat wyników
LeakCanaryAutomatyczny monitoringHeap Dump + stack trace wycieku
Android Memory ProfilerRęczny monitoringWykres pamięci + Heap Dump
MAT (Eclipse)Głęboka analizaRaport Dominator Tree + GC Root path
PerfettoSystem-wide traceOś czasu + native memory

LeakCanary — must-have dla każdego projektu Android. Automatycznie wykrywa wycieki po zakończeniu cyklu życia Activity/Fragment i pokazuje dokładne miejsce wycieku z stack trace. Integracja: jedna linia w build.gradle. LeakCanary 2.x nie wymaga ręcznej inicjalizacji — automatycznie rejestruje Application Watcher.

Jak zapobiegać wyciekom pamięci

Profilaktyka wycieków jest wbudowywana w proces tworzenia poprzez zestaw reguł i narzędzi sprawdzających kod na każdym etapie.

Reguła silnych referencji

Nigdy nie przechowuj referencji do Activity, Fragment lub View w polu statycznym, singletonie lub długożyjącym obiekcie. Jeśli bez referencji nie można się obejść — użyj WeakReference lub przechowuj dane przez ViewModel, który żyje dokładnie tak długo, jak potrzeba, i nie przechowuje View bezpośrednio.

Lifecycle-aware architektura

ViewModel i LiveData z Android Architecture Components rozwiązują problem cyklu życia na poziomie architektury. ViewModel przeżywa obrót ekranu i nie zawiera referencji do View. LiveData automatycznie wypisuje observer przy onDestroy(). Używaj ich zamiast ręcznej subskrypcji usług systemowych.

Code Review z naciskiem na GC Root

Na code review zwracaj uwagę na: pola statyczne z typami Context/View, klasy anonimowe, lambdy zamykające Activity, ręczne subskrypcje, RxJava disposable bez composite, przechowywanie Fragment przez Bundle. W Kotlin dodatkowo sprawdzaj coroutines na launch bez powiązania z cyklem życia.

Automatyczna kontrola w CI

LeakCanary może działać jako część potoku testowego: uruchamiaj testy akceptacyjne z LeakCanary i oznaczaj build jako nieudany, jeśli znaleziono wyciek. Zapobiega to przedostawaniu się wycieków do produkcji. Uzupełnij kontrolę Android Lint z regułą StaticFieldLeak — znajduje ona potencjalne wycieki na poziomie analizy statycznej.

kotlin
// LeakCanary w testach
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // fail jeśli jest wyciek
    }
}

Często zadawane pytania

Czym różni się wyciek pamięci od OutOfMemoryError?

Wyciek to przyczyna, a OutOfMemoryError to skutek. Jeden wyciek nie prowadzi do OOM, ale nagromadzenie dziesiątek wycieków wyczerpuje Heap. OOM to fatalny wyjątek, a wyciek to wzorzec, który do niego prowadzi z upływem czasu.

Jak znaleźć wyciek bez LeakCanary?

Przez Android Memory Profiler: otwieraj i zamykaj ekran 5 razy, po każdym zamknięciu wywołuj GC. Jeśli pamięć nie wraca do poziomu bazowego — jest wyciek. Wykonaj Heap Dump i znajdź na liście klasę Activity, której liczba jest większa niż 0 po zamknięciu.

Czy Kotlin może zapobiegać wyciekom na poziomie języka?

częściowo. Kotlin rozwiązuje problem null-safety, ale nie zarządza strong references. Coroutines z lifecycleScope i viewModelScope zapobiegają wyciekom z zadań w tle, a sealed class i data class zmniejszają liczbę stanów prowadzących do wycieków. Główna ochrona to wzorce architektoniczne, a nie funkcje języka.

Dlaczego LeakCanary znajduje wyciek, którego nie ma?

LeakCanary czasami daje false positive: obiekt może być tymczasowo przechowywany przez system (np. InputMethodManager przechowuje ostatni View). Sprawdź ręcznie: jeśli Retained Size < 1 KB i GC Root to usługa systemowa, prawdopodobnie to fałszywy alarm.

Czy wycieki pamięci występują tylko na Androidzie?

Nie. Wycieki są możliwe na każdej platformie z GC: iOS (Swift/Objective-C), Flutter (Dart), przeglądarki (JavaScript). Mechanizmy są takie same — strong reference od GC Root. W iOS ARC automatycznie zarządza pamięcią, ale retain cycle między obiektami tworzy ten sam wyciek.

Podsumowanie

  • Memory Leak — obiekt, którego GC nie może zwolnić z powodu zapomnianej strong reference
  • Statyczne referencje do Activity i Context — najczęstsza przyczyna wycieków
  • Niejawne referencje przez anonimowe klasy, lambdy i subskrypcje RxJava są bardziej podstępne niż jawne
  • LeakCanary automatycznie znajduje wycieki i pokazuje dokładny stack trace
  • Lifecycle-aware komponenty (ViewModel, LiveData, lifecycleScope) eliminują klasę wycieków
  • Heap Dump i analiza Retained Size — główna metoda ręcznej diagnostyki
  • Profilaktyka obejmuje code review na strong reference i CI sprawdzanie LeakCanary

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również