Skip to content

Latest commit

 

History

History
377 lines (256 loc) · 14.5 KB

File metadata and controls

377 lines (256 loc) · 14.5 KB

Сборщик мусора (Garbage Collector, GC)


Сборщик мусора — это механизм автоматического управления памятью в JVM, который освобождает объекты, ставшие недостижимыми.


Как GC определяет, какие объекты удалять?

Принцип достижимости (Reachability)

Объект считается живым (reachable), если на него есть ссылка из:

  • Стека (локальные переменные, параметры методов).
  • Статических полей класса.
  • JNI-ссылок (для нативного кода).
  • Других живых объектов (например, поля класса).

Если цепочка ссылок разрывается — объект становится мусором.

Алгоритм маркировки (Marking)

  1. Корневые точки (GC Roots):

    • JVM начинает обход с "корней" (стек, статические поля и т.д.).
  2. Обход графа объектов:

    • Все объекты, достижимые из корней, помечаются как живые.
  3. Сборка мусора:

    • Непомеченные объекты удаляются.
// Пример: объект становится мусором
Object obj = new Object(); // Создали объект
obj = null;                // Объект больше не достижим → будет удалён GC

Типы ссылок и их влияние на GC

В Java/Kotlin есть 4 типа ссылок, которые по-разному взаимодействуют с GC:

Тип ссылки Описание Когда удаляется?
Сильная (Strong) Обычные ссылки (val obj = Object()). Только при недостижимости.
Мягкая (Soft) SoftReference — для кэшей. При нехватке памяти.
Слабая (Weak) WeakReference — для ассоциативных данных (например, WeakHashMap). В следующем цикле GC.
Фантомная (Phantom) PhantomReference — для финализации (устаревший подход, лучше Cleaner). После finalize (если был).
// SoftReference (кэш)
val softRef = SoftReference(heavyObject)

// WeakReference (ассоциативные данные)
val weakRef = WeakReference(data)

Как GC работает в многопоточных приложениях?

Проблемы многопоточности

  • Stop-The-World (STW): На время работы GC все потоки приостанавливаются. → Важно минимизировать паузы (особенно для Android UI).
  • Конкуренция за ресурсы: Несколько потоков могут создавать/удалять объекты одновременно.

Алгоритмы GC (и их многопоточная оптимизация)

  1. Serial GC (Single-Thread)
  • Как работает: - Один поток выполняет сборку.
  • Когда использовать: - Для приложений с маленькой кучей (например, утилиты).
  1. Parallel GC (Throughput Collector)
  • Как работает: Несколько потоков параллельно выполняют сборку.
  • Плюсы: Быстрее Serial GC на многопроцессорных системах.
  • Минусы: Всё равно требует STW-пауз.
  1. CMS (Concurrent Mark-Sweep)
  • Как работает: Большая часть работы (маркировка) выполняется без остановки приложения.
  • Проблемы: Требует больше CPU, возможна фрагментация памяти.
  1. G1 (Garbage-First)
  • Как работает: Делит кучу на регионы, собирает мусор в фоне.
  • Плюсы: Предсказуемые паузы (подходит для Android).
  1. ZGC / Shenandoah (Low-Latency)
  • Как работает: Практически без STW-пауз (паузы < 1 мс).
  • Когда использовать: Для высоконагруженных серверов (не поддерживается в Android).

Финанализация и её проблемы

Метод finalize()

Вызывается перед удалением объекта (если переопределён).

Проблемы:

  • Не гарантируется вызов (если GC не соберёт объект).
  • Замедляет GC (объект с finalize() проходит два цикла сборки).
  • Может "воскресить" объект (через сильную ссылку внутри finalize()).

Совет: Вместо finalize() используйте Cleaner или PhantomReference.

// Плохо (устаревший способ)
@Override
protected void finalize() throws Throwable {
   releaseResources();
}

// Лучше (Java 9+)
Cleaner cleaner = Cleaner.create();
cleaner.register(this, () -> releaseResources());

Как GC взаимодействует с Kotlin Coroutines?

Лямбды и замыкания

Лямбды корутин могут захватывать контекст, что иногда приводит к утечкам:

scope.launch {
    val data = fetchData() // Захватывает 'scope'
    println(data)
}

Решение: Используйте coroutineScope или viewModelScope для автоматической отмены.

DisposableHandle

Для ресурсов, которые нужно освобождать:

val job = scope.launch { ... }
job.invokeOnCompletion { releaseResources() }

Сборщик мусора (GC) в Android/Kotlin: Особенности работы с корутинами

В Android используется виртуальная машина ART (Android Runtime) с улучшенным GC по сравнению с классической JVM.


Особенности GC в Android

Тип сборщика мусора

  • До Android 5.0 (Lollipop): CMS (Concurrent Mark-Sweep) с длительными STW-паузами.
  • Android 5.0+ (ART): Generational GC (похож на G1 в JVM):
  • Молодая генерация (Young Generation): Частая сборка (алгоритм Copying).
  • Старая генерация (Old Generation): Редкая сборка (Mark-Sweep или Mark-Compact).

Проблемы GC в Android

  • Stop-The-World (STW) паузы: При сборке старой генерации UI может "зависать".
  • Трассировка ссылок: GC должен обходить все живые объекты, включая корутины.

Корутины и GC: Как избежать утечек

Корутины могут неявно удерживать ссылки на объекты, что приводит к утечкам памяти.

1. Захват контекста в лямбдах

Проблема: Лямбда корутины захватывает внешний класс (например, Activity), продлевая его жизнь.

class MyActivity : AppCompatActivity() {
    override fun onCreate() {
        lifecycleScope.launch {
            val data = fetchData() // Лямбда захватывает MyActivity!
            updateUI(data)         // Если Activity уничтожена — утечка!
        }
    }
}

Решение: Используйте viewModelScope или lifecycleScope + проверку isActive:

lifecycleScope.launch {
    if (!isActive) return@launch // Проверка перед долгой операцией
    val data = fetchData()
    withContext(Dispatchers.Main) {
        if (isActive) updateUI(data) // Дополнительная проверка
    }
}

2. Job и отмена корутин

Неотменённые корутины удерживают ссылки на свои контексты.

Плохо:

val job = CoroutineScope(Dispatchers.IO).launch {
    heavyOperation() // Корутина живёт даже после закрытия Activity
}

Хорошо:

private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

override fun onDestroy() {
    scope.cancel() // Явная отмена при уничтожении
    super.onDestroy()
}

WeakReference для колбэков

Если корутина принимает колбэки из Activity, используйте WeakReference:

class MyViewModel : ViewModel() {
    private var callback: WeakReference<MyCallback>? = null

    fun registerCallback(cb: MyCallback) {
        callback = WeakReference(cb)
    }

    fun fetchData() {
        viewModelScope.launch {
            val data = repo.loadData()
            callback?.get()?.onDataLoaded(data) // Не удерживает Activity
        }
    }
}

Лучший способ привязать корутины и ViewModel к жизненному циклу и области

viewModel.state
   .flowWithLifecycle(lifecycle = viewLifecycleOwner.lifecycle)
   .onEach { state: State ->
   }
    .launchIn(scope = viewLifecycleOwner.lifecycleScope)

Как GC обрабатывает корутины?

Структура корутины в памяти

  • Continuation: Объект, хранящий состояние корутины (локальные переменные, точка возобновления).
  • Context: Ссылки на Dispatcher, Job, CoroutineScope.

Что делает GC:

  • Если корутина завершена или отменена — её объекты помечаются как мусор.
  • Если корутина активна — все её объекты достижимы из корней (Job, Dispatcher).

Особенности для Dispatchers

  • Dispatchers.Main (UI): Корутины здесь имеют высокий приоритет, GC старается не трогать их во время работы.
  • Dispatchers.IO: Долгие операции могут создавать много временных объектов → частые сборки в молодой генерации.

Достижимые (Reachable) и недостижимые (Unreachable) объекты в Java/Kotlin


Эти термины определяют, может ли сборщик мусора (GC) удалить объект из памяти.


Достижимые объекты (GC их НЕ удалит)

Объект считается достижимым, если на него существует цепочка ссылок, начинающаяся от "корней" (GC Roots).

Основные GC Roots:

  • Локальные переменные в методах (живут в стеке потока).
  • Активные потоки (Thread).
  • Статические поля классов.
  • JNI-ссылки (для нативного кода).

Пример достижимого объекта:

fun main() {
    val user = User("Alex")  // `user` — ссылка из стека (GC Root)
    println(user.name)
}
// Пока выполняется main(), объект `User` достижим.

Недостижимые объекты (GC их удалит)

Объект становится недостижимым, если нет ни одной ссылки из GC Roots.

Пример недостижимого объекта:

fun main() {
    var user = User("Alex")  // Ссылка из стека
    user = null              // Обрыв ссылки
    // Теперь объект `User("Alex")` недостижим — GC его удалит.
}

Как GC определяет достижимость?

Алгоритм работы:

  1. Маркировка (Mark):

    • GC обходит все GC Roots (стек, статические поля и т.д.).
    • Помечает все объекты, до которых можно добраться по ссылкам.
  2. Очистка (Sweep):

    • Удаляет непомеченные (недостижимые) объекты.

Визуализация:

GC Roots (стек, статик-поля)
    │
    ├── Объект A (достижим) → Объект B (достижим)
    │
    └── Объект C (достижим)

Объекты D и E, не связанные с корнями, будут удалены.


Особые случаи

Циклические ссылки

Даже если объекты ссылаются друг на друга, но нет связи с GC Roots — они недостижимы.

class Node(var next: Node?)

fun main() {
    val node1 = Node(null)
    val node2 = Node(node1)
    node1.next = node2  // Цикл: node1 ↔ node2
    // Но если нет ссылки из GC Roots — оба будут удалены.
}

WeakReference и достижимость

Объект, на который есть только слабая ссылка (WeakReference), считается недостижимым и будет удалён.

val weakRef = WeakReference(User("Alex"))
println(weakRef.get()?.name)  // Может вернуть `null` после GC.