Garbage Collection (GC): какво е, алгоритми и събиране на отпадъци в мобилната разработка

Автор: IT Sectr Публикувано: 2026-03-29 Време за четене: 10 мин

Автоматичното управление на паметта чрез събиране на отпадъци е ключов механизъм на платформата Android, основан на виртуалната машина ART. Според Google Android Documentation, 2026, събирачът на отпадъци освобождава разработчика от ръчното управление на паметта, като автоматично премахва обекти, към които вече няма референции. Без GC всяко заделяне на обект би изисквало изрично извикване на free или delete, което в Java екосистемата с милиони обекти в секунда е физически невъзможно.

Основни изводи

  • Garbage Collection — автоматичен механизъм за освобождаване на памет чрез изтриване на неизползвани обекти в Java и Android
  • Основни алгоритми — Mark-and-Sweep, Copying Collection и Generational Collection определят ефективността на събирането
  • ART и Dalvik — две реализации на виртуална машина за Android, като ART (Android Runtime) замени Dalvik от Android 5.0
  • GC Pauses — спирания на изпълнението на приложението по време на събиране — основна причина за jank и проблеми с производителността
  • Оптимизация на GC — намаляване на алокациите, използване на обектни пулове и правилен избор на Collection типове намаляват натоварването на събирача

Какво е Garbage Collection (GC)?

Garbage Collection (GC) — това е автоматичен процес на откриване и освобождаване на памет, заета от обекти, които вече не се използват от програмата. В контекста на мобилната разработка, GC се прилага в платформата Android чрез виртуалната машина ART, както и в стандартната Java Virtual Machine.

За разлика от езиците с ръчно управление на паметта (C, C++), където програмистът трябва изрично да вика free или delete, GC поема напълно проследяването на жизнения цикъл на обектите. Разработчикът създава нови обекти чрез оператора new, а събирачът определя кога даден обект става недостижим — тоест няма нито една активна референция към него.

Основна метрика за ефективност на GC — време на пауза (pause time) и пропускателна способност (throughput). Паузата е интервалът, през който се спира изпълнението на приложението за провеждане на събиране. В мобилна среда паузи над 8–16 милисекунди се забелязват като пропуснати кадри (jank).

Според Google I/O 2019, ART в Android 10 намали типичните GC паузи до 2–4 ms, което е 70% по-малко в сравнение с Dalvik в Android 4.4. Въпреки това, неправилната работа с паметта — често заделяне на обекти в цикли, създаване на временни инстанции без необходимост — остава основна причина за проблеми с производителността.

Как работи събирачът на отпадъци: основни алгоритми

Всички реализации на GC в Java и Android се основават на няколко фундаментални алгоритъма, които се комбинират за постигане на баланс между време на пауза и пълнота на почистването. Разбирането на тези алгоритми е необходимо за писане на GC-friendly код.

Mark-and-Sweep

Mark-and-Sweep — най-простият алгоритъм, работещ в две фази. Във фазата Mark събирачът обхожда графа от обекти, започвайки от кореновите референции (root set) — локални променливи, статични полета, стекове на нишки. Всеки достижим обект се маркира с флаг live. Във фазата Sweep събирачът обхожда цялата купчина и освобождава паметта на немаркираните обекти.

Недостатък — фрагментация на паметта: след Sweep свободните участъци се редуват със заети, което затруднява заделянето на големи обекти. В мобилни сценарии това е критично, тъй като купчината обикновено е малка (64–512 MB на Android).

Copying Collection

Copying Collection разделя купчината на две полупространства (semi-spaces). Активните обекти се копират от едното полупространство в другото компактно, без празнини. След копирането старото полупространство се обявява изцяло за свободно. Алгоритъмът напълно елиминира фрагментацията, но изисква двойно повече памет.

В мобилни среди Copying Collection се прилага от поколенчески събирачи за бързо почистване на млади обекти, които статистически умират рано (хипотеза за слабото поколение).

Generational Collection

Generational Collection разделя купчината на поколения: Young Generation (млади обекти) и Old Generation (стари, преживели няколко събирания). Събирането на младото поколение (Minor GC) се изпълнява често и бързо, тъй като повечето обекти умират млади. Събирането на старото поколение (Major GC или Full GC) става по-рядко, но продължава по-дълго.

java
// Демонстрация на generational GC: младите обекти умират бързо
void processItems(List<Item> items) {
    List<Result> results = new ArrayList<>();       // живее през целия метод
    for (Item item : items) {
        Result r = new Result(item.getValue());    // умира моментално
        if (r.isValid()) {
            process(r);                               // r става боклук
        }
    }
    saveResults(results);                             // results преминава към Old Gen
}

В този пример обектите Result се създават вътре в цикъла и веднага стават боклук — те са идеални кандидати за Young GC. Обектът results живее по-дълго и мигрира към Old Generation. Разделянето на поколения позволява на Minor GC да почиства млади обекти за милисекунди, без да докосва старата купчина.

Garbage Collection в Android: ART и Dalvik

Android премина от Dalvik VM към ART (Android Runtime) и реализацията на GC е една от ключовите разлики между тях. Разбирането на архитектурата на GC в Android помага за писане на код, който минимизира паузите на реални устройства.

ХарактеристикаDalvik (до 4.4)ART (5.0+)
Тип GCMark-and-Sweep с Concurrent MarkGenerational + Concurrent
Типична пауза10–30 ms2–4 ms
КомпактизацияНе (само нарастваща фрагментация)Да (на заден фон, без спиране на приложението)
AOT-компилацияJIT (Just-In-Time)AOT + JIT (хибридна)

Dalvik GC

Dalvik използваше комбинация от Mark-and-Sweep с concurrent фаза. Concurrent Mark позволяваше на приложението да продължи работа по време на обхождане на графа от обекти, но фазата Sweep изискваше спиране на всички нишки (Stop-The-World). На устройства с малко RAM (512 MB – 1 GB) паузите достигаха 30 ms, причинявайки забележими забавяния на интерфейса. Освен това, Dalvik не компактизираше купчината, така че след продължителна работа фрагментацията нарастваше и заделянето на големи обекти (напр. Bitmap) можеше да предизвика OutOfMemoryError въпреки достатъчното общо количество свободна памет.

ART GC

ART (Android Runtime) въведе поколенчески събирач с concurrent компактизация. Купчината се разделя на три региона: Young, Mature (аналог на Old Generation) и Large Object Space (за обекти с размер над 12 KB). Събирането на Young Region става паралелно без спиране на нишките в повечето случаи. В Android 10+ се появи Concurrent Copying — компактизация, която се изпълнява във фонова нишка без Stop-The-World.

Благодарение на архитектурата на ART, типичните GC паузи намаляха до 2–4 ms, а в сценарии с преобладаване на млади обекти — до 0.5–1 ms. Това позволи на Android устройствата да осигуряват стабилни 60 FPS дори при активна работа с паметта.

Типове събирачи на отпадъци в Java

В екосистемата Java съществуват няколко реализации на GC, всяка със свой профил на производителност. За Android разработка изборът е ограничен до ART, но познаването на Java GC е полезно при писане на сървърна част на мобилни приложения и при разработка с Kotlin Multiplatform.

Serial GC

Serial GC — еднонишков събирач с пълно спиране на приложението (Stop-The-World). Всяка операция Mark, Sweep и Compact се изпълнява от една нишка. Производителността е ниска — не се прилага за мобилни сървъри. Подходящ само за малки приложения с купчина до 100 MB.

Parallel GC

Parallel GC (известен също като Throughput Collector) използва множество нишки за всички фази на събирането. Фокусира се върху максимална пропускателна способност (throughput) — минимизира времето, прекарано в GC спрямо времето за работа на приложението. Включва се чрез флага -XX:+UseParallelGC в JVM.

G1 GC

G1 (Garbage-First) GC — стандартният събирач в Java 9+. Купчината се разделя на региони от 1–32 MB. G1 прогнозира времето на пауза и се стреми да се вмести в зададения лимит (по подразбиране 200 ms). Приоритет: първо се почистват регионите с най-голямо количество отпадъци (оттам и името). G1 е ефективен за сървъри с голяма купчина (4–64 GB) и предвидими паузи.

java
// Включване на G1 GC с целева пауза от 100 ms
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar

public class MemoryMonitor {
    private static final long THRESHOLD = 512 * 1024 * 1024; // 512 MB

    public void checkHeapUsage() {
        Runtime rt = Runtime.getRuntime();
        long used = rt.totalMemory() - rt.freeMemory();
        if (used > THRESHOLD) {
            System.out.println("Heap usage exceeded threshold: " + used);
            System.out.println("Consider reducing allocations");
        }
    }
}

Мониторингът на купчината чрез Runtime позволява ранно откриване на изтичане на памет. Ако used надвишава 80% от максималната купчина при стабилна работа — това е сигнал за възможно изтичане или прекомерна консумация на памет от приложението.

Проблеми на GC и оптимизация на паметта в мобилни приложения

Дори модерният ART GC не решава всички проблеми — неправилното използване на паметта остава основна причина за jank и ANR (Application Not Responding). Нека разгледаме основните сценарии и методи за оптимизация.

GC Pauses и Jank

GC Pauses — спирания на нишките на приложението по време на събиране. На екрана това се проявява като пропуснати кадри, когато времето между два кадъра надвишава 16.6 ms (60 FPS). Ако GC продължава 30 ms, се рисува само един кадър вместо два — потребителят вижда дръпване на интерфейса.

Основни причини за дълги паузи: голям брой живи обекти в Old Generation, фрагментация на купчината, чести Full GC. За диагностика се използват Android Studio Profiler и systrace.

Намаляване на натоварването върху GC

Основно правило за GC-friendly код — минимизиране на броя алокирани обекти. Всеки нов обект изисква не само заделяне на памет, но и последващо събиране. Дори ако GC е бърз, 1000 излишни алокации в секунда означават 1000 проверки за събирача.

  • Избягвайте създаването на обекти в цикли — изнесете създаването извън цикъла, използвайте повторно локални променливи
  • Използвайте обектни пулове — за Bitmap, byte[] и други тежки структури използвайте Object Pool или RecyclerView.ViewHolder
  • Предпочитайте примитиви — int вместо Integer, float вместо Float избягват автоматичното опаковане (autoboxing)
  • Използвайте SparseArray — вместо HashMap<Integer, V> в Android SDK има SparseArray, LongSparseArray, които работят с примитиви
  • StringBuilder вместо конкатенация — всяко събиране на низове създава нов обект String

Изтичане на памет

Изтичане на памет възниква, когато обект остава достижим, въпреки че вече не е нужен. GC не може да изтрие такъв обект и паметта постепенно се изчерпва. Типични причини: неотписани слушатели, статични референции към Activity, анонимни класове, улавящи външен контекст, и незатворени Cursor/InputStream.

java
// Изтичане на памет: анонимен клас държи референция към Activity
public void startTask() {
    new Thread(new Runnable() {                    // неявно държи this (Activity)
        @Override
        public void run() {
            // продължителна операция...
            System.out.println("Done");
        }
    }).start();
}

// Поправка: статичен вложен клас + WeakReference
private static class TaskRunnable implements Runnable {
    private WeakReference<Activity> activityRef;

    TaskRunnable(Activity activity) {
        this.activityRef = new WeakReference<>(activity);
    }

    @Override
    public void run() {
        Activity act = activityRef.get();
        if (act != null) {
            // безопасна работа с Activity
        }
    }
}

В този пример анонимният Runnable улавя неявна референция към Activity. Докато нишката е жива — Activity не може да бъде събрано от GC, дори ако потребителят вече е затворил екрана. Поправката WeakReference + static class прекъсва тази верига и позволява на Activity да бъде освободено.

Често задавани въпроси

Как се различава GC в Android от GC в Java?

GC в Android (ART) е поколенчески събирач с concurrent компактизация, оптимизиран за мобилни устройства с ограничена памет. Java GC (G1, ZGC) са сървърни събирачи с големи купчини и предвидими паузи. ART GC не използва JVM флагове — цялата настройка се извършва автоматично на ниво операционна система.

Какво е Stop-The-World в GC?

Stop-The-World — моментът, когато събирачът спира всички нишки на приложението, за да обходи безопасно графа от обекти или да освободи памет. Колкото по-дълго е STW, толкова по-забележим е jank. ART намали типичното време на STW до 2–4 ms благодарение на поколенческата архитектура.

Как да открием изтичане на памет в Android?

Използвайте Android Studio Memory Profiler — той показва растежа на купчината, броя алокации и позволява правене на Heap Dump. За задълбочен анализ използвайте LeakCanary — библиотека, която автоматично открива изтичания и показва веригата от референции, препятстващи GC събирането.

Кога се случва Full GC и защо е опасен?

Full GC — пълно събиране на всички поколения на купчината, включително Old Generation. В мобилни приложения Full GC може да продължи 50–200 ms, причинявайки забележим jank или ANR. Основни причини: фрагментация на купчината, изтичане на памет, превишаване на прага на Old Generation.

Как Kotlin помага за избягване на изтичане на памет?

Kotlin предоставя корутини със структурирана конкурентност — отмяната на scope автоматично отменя всички дъщерни корутини, предотвратявайки изтичания. Също така в Kotlin има делегат lazy за мързелива инициализация и оператори за обхват, които намаляват броя на временните обекти.

Резюме

  • Garbage Collection — автоматично управление на паметта чрез премахване на недостижими обекти, основа на Android Runtime
  • Mark-and-Sweep — основен алгоритъм с двуфаэно събиране, страда от фрагментация на купчината
  • Copying Collection — елиминира фрагментацията чрез копиране на живи обекти в компактно полупространство
  • Generational GC — разделя купчината на поколения (Young/Old), ускорявайки събирането на млади краткоживеещи обекти
  • ART в Android — поколенчески събирач с concurrent компактизация и паузи 2–4 ms, заменил Dalvik в Android 5.0
  • Оптимизация на GC — намаляване на алокациите, обектни пулове, примитиви вместо обвивки и SparseArray вместо HashMap намаляват натоварването на събирача
  • Диагностика — Android Studio Profiler, systrace и LeakCanary са основните инструменти за откриване на проблеми с паметта

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също