OutOfMemoryError — fatalny wyjątek, który pojawia się, gdy maszyna wirtualna Java (JVM) lub Android Runtime (ART) nie może przydzielić pamięci dla nowego obiektu z powodu braku miejsca na stercie (Heap). Według Square Engineering, 70% OutOfMemoryError w aplikacjach mobilnych jest spowodowanych wyciekami pamięci, a nie rzeczywistym przekroczeniem limitu. Zrozumienie przyczyn OOM to klucz do stabilnej pracy aplikacji.
Najważniejsze
OutOfMemoryError (OOM) — to wyjątek z rodziny VirtualMachineError w Java/Kotlin, który sygnalizuje niemożność przydzielenia pamięci dla nowego obiektu. W przeciwieństwie do wyjątków checked, OOM jest błędem (Error) i nie wymaga obsługi przez catch — choć technicznie można go złapać. Po wystąpieniu OOM aplikacja zwykle znajduje się w niestabilnym stanie i zaleca się jej zamknięcie.
Na Androidzie każda aplikacja ma limit Heap ustawiony przez producenta urządzenia. Dla nowoczesnych smartfonów z 6+ GB RAM limit wynosi 256–512 MB, dla budżetowych urządzeń — 128–192 MB. Gdy łączny rozmiar wszystkich żywych obiektów przekracza ten limit, ART wyrzuca OutOfMemoryError.
Ważne jest zrozumienie: OOM nie zawsze oznacza, że w urządzeniu skończyła się fizyczna pamięć. Oznacza to, że aplikacja wyczerpała swój limit Heap ustawiony przez system. Inne aplikacje mogą mieć wolną pamięć, ale twoja aplikacja nie może jej użyć z powodu izolacji procesów w Androidzie.
Pięć scenariuszy regularnie prowadzi do OOM w aplikacjach mobilnych. Każdy scenariusz jest związany z określonym typem danych lub operacją.
Bitmap — główny konsument pamięci w aplikacjach Android. Załadowanie obrazu FullHD (1920 × 1080) w oryginalnym rozmiarze zajmuje 8.3 MB w formacie ARGB_8888. Jeśli w RecyclerView jest 50 takich obrazów — to 415 MB, co przekracza Heap każdego urządzenia. Ładowanie obrazów bez inSampleSize — gwarantowany OOM na słabych urządzeniach.
Używaj Glide lub Coil do automatycznego skalowania. Te biblioteki ładują obrazy w rozmiarze dopasowanym do View, a nie do oryginalnej rozdzielczości. Do bezpośredniego użycia BitmapFactory.Options stosuj inSampleSize: oblicz go jako potęgę dwójki, tak aby końcowy rozmiar nie przekraczał 2048 × 2048 pikseli. Dodatkowo używaj RGB_565 zamiast ARGB_8888 dla obrazów bez przezroczystości — to dwukrotnie zmniejsza zużycie pamięci.
fun loadScaledBitmap(path: String, reqWidth: Int): Bitmap? {
val opts = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, opts)
opts.inSampleSize = calculateSampleSize(opts.outWidth, reqWidth)
opts.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, opts)
}
Jeden wyciek kilku KB nie spowoduje OOM. Ale dziesiątki wycieków na każdym ekranie narastają: każde przejście na ekran dodaje wyciek, GC nie może zwolnić obiektów, Heap się zapełnia. Typowy wzorzec: użytkownik otwiera i zamyka ekran profilu 20 razy → Heap rośnie o 200 MB → aplikacja pada z OOM.
Zainstaluj LeakCanary w projekcie do automatycznego wykrywania wycieków. Pokaże on każdy wyciekający obiekt z dokładnym stack trace. Po naprawieniu wszystkich wycieków zużycie Heap stanie się stabilne: po zamknięciu ekranu pamięć wraca do podstawowego poziomu.
Ładowanie plików w całości do byte[] — prosta droga do OOM. Plik JSON o rozmiarze 50 MB podczas parsowania utworzy string tego samego rozmiaru plus model DOM. Pliki wideo załadowane do pamięci, bufory audio i duże zbiory danych protobuf — wszystkie mogą przekroczyć limit Heap w jednej operacji.
Przetwarzaj duże dane strumieniami: InputStream z buforem 4–8 KB, Streaming JSON- parser (Jackson lub Gson z JsonReader), MediaCodec dla wideo. Nigdy nie wywołuj File.readBytes() na plikach większych niż 10% dostępnego Heap.
Intensywne tworzenie obiektów w pętli bez pośredniego GC może prowadzić do OOM, szczególnie na urządzeniach z małym Heap. Przykład: generowanie 100 000 obiektów w pętli for, które nie mieszczą się w Heap zanim GC zdąży je zebrać. Częściej występuje w grach i edytorach graficznych.
Używaj Object Pool dla obiektów, które są masowo tworzone i niszczone. Dla danych numerycznych używaj typów prymitywnych (FloatArray zamiast List<Float>). RecyclerView z ViewHolder Pool rozwiązuje ten problem dla komponentów UI.
Fragmentacja — stan, w którym wolnej pamięci jest łącznie wystarczająco, ale nie ma ciągłego bloku dla nowego obiektu. ART kompaktuje Heap podczas GC, ale nie zawsze z powodzeniem. Duże tablice (Bitmap, byte[]) są najbardziej wrażliwe na fragmentację.
ART na Androidzie 8+ używa Generational GC, który zmniejsza fragmentację poprzez podział na młode i stare obiekty. Mimo to, unikaj alokowania fragmentów różnych rozmiarów w jednej puli — staraj się używać pre-allocated buforów o stałym rozmiarze.
Limit Heap w Androidzie nie jest stałą — zależy od producenta, modelu urządzenia i wersji OS. Google określa minimalne wymagania poprzez Compatibility Definition Document (CDD), ale producenci ustalają rzeczywiste wartości.
| Kategoria urządzeń | Typowy Heap | largeHeap |
|---|---|---|
| Budżetowe (1–2 GB RAM) | 128–192 MB | 256–384 MB |
| Średnie (3–4 GB RAM) | 256–384 MB | 512 MB |
| Flagowe (6+ GB RAM) | 384–512 MB | 768 MB–1 GB |
| Tablety (4+ GB RAM) | 256–512 MB | 768 MB |
| Wear OS | 32–64 MB | Brak |
Można poprosić o zwiększony limit przez android:largeHeap="true" w manifeście. Używaj go ostrożnie: zwiększenie Heap nie rozwiązuje problemu wycieków i może pogorszyć doświadczenie użytkownika, jeśli system będzie zmuszony zabijać inne aplikacje, aby zwolnić pamięć dla twojej. Dla Wear OS limit Heap jest minimalny — zaledwie 32–64 MB, tutaj largeHeap jest niedostępny, a oszczędność pamięci jest podwójnie krytyczna.
Diagnostyka OOM wymaga analizy Heap Dump i zrozumienia, które obiekty zużywają pamięć. Android Studio dostarcza wszystkie niezbędne narzędzia.
Krok 1: Złap moment OOM. W Android Memory Profiler naciśnij Record memory allocations i wykonaj scenariusz, który powoduje crash. Profiler pokaże skok alokacji przed OOM. Jeśli OOM się nie odtwarza, zmniejsz Heap przez android:smallHeap w debug-buildzie lub użyj DDMS z ręcznym wywołaniem GC.
Krok 2: Wykonaj zrzut Heap Dump w momencie szczytowego obciążenia (przed OOM). Otwórz Dump w Android Studio: zakładka Classes posortowana według Retained Size. Największe obiekty — Bitmap, byte[], String. Dla każdego Bitmap sprawdź rozmiar (width × height × 4 bajty) i ścieżkę ładowania przez Stack Trace.
Krok 3: Analizuj liczbę powtarzających się obiektów. Jeśli widzisz 200 identycznych Fragment lub Activity — to wyciek. Jeśli 500 Bitmap o tym samym rozmiarze — problem z buforowaniem obrazów. MAT (Memory Analyzer Tool) zapewnia głębszą analizę z Dominator Tree, pokazującą, które obiekty utrzymują 80% Heap.
// Polecenie do zrzutu Heap Dump przez adb
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof.
Kompleksowa strategia zapobiegania OOM obejmuje pięć poziomów ochrony: od rozwiązań architektonicznych po monitoring na produkcji.
ViewModel + Repository wzorzec oddziela dane od UI i zapobiega utrzymywaniu View przy obrocie ekranu. ViewModel przetrwa Activity, jego dane nie są tracone, a View może być odtworzone bez duplikowania danych w pamięci. Używaj StateFlow zamiast LiveData do jawnego zarządzania stanami.
Glide — obowiązkowa biblioteka do pracy z obrazami. Automatycznie skaluje, buforuje (dysk + pamięć) i recycluje Bitmap. Skonfiguruj diskCacheStrategy i skipMemoryCache dla dużych list. Dla animowanych obrazów używaj Glide z GIF/WebP — zajmują mniej pamięci niż sekwencja Bitmap.
Firebase Performance Monitoring śledzi zużycie pamięci w czasie rzeczywistym. Ustaw alert na zajęcie Heap powyżej 80% limitu — to sygnał do sprawdzenia. Crashlytics zbiera OOM jako wyjątek i pokazuje ostatni znany stan Heap przed crashem. Dla Android 11+ używaj ApplicationExitInfo do detekcji zakończeń OOM.
Obowiązkowo testuj aplikację na urządzeniach z minimalnym Heap (128–192 MB). Emulator z małym ekranem i małym Heap emuluje budżetowe urządzenie. Jeśli aplikacja działa na takim urządzeniu, na flagowcach problemów z OOM nie będzie. Używaj Firebase Test Lab z rzeczywistymi urządzeniami różnych kategorii cenowych.
// Sprawdzenie dostępnego Heap przed ciężką operacją
fun canAllocate(requiredBytes: Long): Boolean {
val runtime = Runtime.getRuntime()
val free = runtime.freeMemory()
return free > requiredBytes * 2 // zapas 50%
}
Często zadawane pytania
Technicznie tak, ale nie jest to zalecane. Po OOM aplikacja znajduje się w niestabilnym stanie: nowe alokacje mogą nie działać, a niektóre obiekty mogą być częściowo utworzone. Jedyną rozsądną akcją w catch jest logowanie i restart Activity.
Limit Heap jest różny na różnych urządzeniach. Operacja, która wymaga 300 MB, upadnie na urządzeniu z limitem 192 MB, ale przejdzie na flagowcu z 512 MB. Testuj na urządzeniach z minimalnymi parametrami, aby wykryć scenariusze OOM.
largeHeap zwiększa limit, ale nie przyspiesza aplikacji. Pauzy GC stają się dłuższe, ponieważ zbieranie dużego Heap trwa dłużej. System może zabijać aplikacje w tle, aby zapewnić pamięć. Używaj largeHeap tylko dla aplikacji, które obiektywnie potrzebują dużo pamięci (kamery, edytory).
OOM — wyjątek wewnątrz aplikacji przy braku Heap. Systemowe zabicie (Low Memory Killer) — decyzja jądra Linux o zabiciu procesu w celu zwolnienia pamięci dla innych aplikacji. Przy systemowym zabiciu aplikacja nie otrzymuje wyjątku — proces po prostu się kończy.
Wzór: width × height × bytesPerPixel. ARGB_8888 = 4 B/piksel, RGB_565 = 2 B/piksel. FullHD Bitmap (1920 × 1080) w ARGB_8888 = 8.3 MB. 4K Bitmap (3840 × 2160) = 33 MB. Zawsze skaluj obrazy do rozmiaru niezbędnego do wyświetlenia na ekranie.
Podsumowanie
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.
Przeczytaj również