Сборщик мусора — это механизм автоматического управления памятью в JVM
, который освобождает объекты, ставшие недостижимыми.
Объект считается живым (reachable
), если на него есть ссылка из:
- Стека (локальные переменные, параметры методов).
- Статических полей класса.
JNI
-ссылок (для нативного кода).- Других живых объектов (например, поля класса).
Если цепочка ссылок разрывается — объект становится мусором.
-
Корневые точки (GC Roots):
JVM
начинает обход с "корней" (стек, статические поля и т.д.).
-
Обход графа объектов:
- Все объекты, достижимые из корней, помечаются как живые.
-
Сборка мусора:
- Непомеченные объекты удаляются.
// Пример: объект становится мусором
Object obj = new Object(); // Создали объект
obj = null; // Объект больше не достижим → будет удалён 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)
- Stop-The-World (STW): На время работы
GC
все потоки приостанавливаются. → Важно минимизировать паузы (особенно дляAndroid UI
). - Конкуренция за ресурсы: Несколько потоков могут создавать/удалять объекты одновременно.
- Serial GC (Single-Thread)
- Как работает: - Один поток выполняет сборку.
- Когда использовать: - Для приложений с маленькой кучей (например, утилиты).
- Parallel GC (Throughput Collector)
- Как работает: Несколько потоков параллельно выполняют сборку.
- Плюсы: Быстрее
Serial GC
на многопроцессорных системах. - Минусы: Всё равно требует
STW
-пауз.
- CMS (Concurrent Mark-Sweep)
- Как работает: Большая часть работы (маркировка) выполняется без остановки приложения.
- Проблемы: Требует больше
CPU
, возможна фрагментация памяти.
- G1 (Garbage-First)
- Как работает: Делит кучу на регионы, собирает мусор в фоне.
- Плюсы: Предсказуемые паузы (подходит для
Android
).
- ZGC / Shenandoah (Low-Latency)
- Как работает: Практически без
STW
-пауз (паузы < 1 мс). - Когда использовать: Для высоконагруженных серверов (не поддерживается в
Android
).
Вызывается перед удалением объекта (если переопределён).
Проблемы:
- Не гарантируется вызов (если
GC
не соберёт объект). - Замедляет
GC
(объект сfinalize()
проходит два цикла сборки). - Может "воскресить" объект (через сильную ссылку внутри
finalize()
).
Совет: Вместо finalize()
используйте Cleaner
или PhantomReference
.
// Плохо (устаревший способ)
@Override
protected void finalize() throws Throwable {
releaseResources();
}
// Лучше (Java 9+)
Cleaner cleaner = Cleaner.create();
cleaner.register(this, () -> releaseResources());
Лямбды корутин могут захватывать контекст, что иногда приводит к утечкам:
scope.launch {
val data = fetchData() // Захватывает 'scope'
println(data)
}
Решение: Используйте coroutineScope
или viewModelScope
для автоматической отмены.
Для ресурсов, которые нужно освобождать:
val job = scope.launch { ... }
job.invokeOnCompletion { releaseResources() }
В Android
используется виртуальная машина ART
(Android Runtime
) с улучшенным GC
по сравнению с классической JVM
.
- До 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
).
- Stop-The-World (STW) паузы: При сборке старой генерации
UI
может "зависать". - Трассировка ссылок:
GC
должен обходить все живые объекты, включая корутины.
Корутины могут неявно удерживать ссылки на объекты, что приводит к утечкам памяти.
Проблема: Лямбда корутины захватывает внешний класс (например, 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) // Дополнительная проверка
}
}
Неотменённые корутины удерживают ссылки на свои контексты.
Плохо:
val job = CoroutineScope(Dispatchers.IO).launch {
heavyOperation() // Корутина живёт даже после закрытия Activity
}
Хорошо:
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun onDestroy() {
scope.cancel() // Явная отмена при уничтожении
super.onDestroy()
}
Если корутина принимает колбэки из 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.state
.flowWithLifecycle(lifecycle = viewLifecycleOwner.lifecycle)
.onEach { state: State ->
}
.launchIn(scope = viewLifecycleOwner.lifecycleScope)
- Continuation: Объект, хранящий состояние корутины (локальные переменные, точка возобновления).
- Context: Ссылки на
Dispatcher
,Job
,CoroutineScope
.
Что делает GC:
- Если корутина завершена или отменена — её объекты помечаются как мусор.
- Если корутина активна — все её объекты достижимы из корней (
Job
,Dispatcher
).
- Dispatchers.Main (UI): Корутины здесь имеют высокий приоритет,
GC
старается не трогать их во время работы. - Dispatchers.IO: Долгие операции могут создавать много временных объектов → частые сборки в молодой генерации.
Эти термины определяют, может ли сборщик мусора (GC
) удалить объект из памяти.
Объект считается достижимым, если на него существует цепочка ссылок, начинающаяся от "корней" (GC Roots).
- Локальные переменные в методах (живут в стеке потока).
- Активные потоки (
Thread
). - Статические поля классов.
JNI
-ссылки (для нативного кода).
Пример достижимого объекта:
fun main() {
val user = User("Alex") // `user` — ссылка из стека (GC Root)
println(user.name)
}
// Пока выполняется main(), объект `User` достижим.
Объект становится недостижимым, если нет ни одной ссылки из GC Roots
.
Пример недостижимого объекта:
fun main() {
var user = User("Alex") // Ссылка из стека
user = null // Обрыв ссылки
// Теперь объект `User("Alex")` недостижим — GC его удалит.
}
-
Маркировка (
Mark
):GC
обходит всеGC Roots
(стек, статические поля и т.д.).- Помечает все объекты, до которых можно добраться по ссылкам.
-
Очистка (
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
), считается недостижимым и будет удалён.
val weakRef = WeakReference(User("Alex"))
println(weakRef.get()?.name) // Может вернуть `null` после GC.