← Назад к вопросам

Может ли сборщик мусора собрать висящую подписку?

1.7 Middle🔥 183 комментариев
#JVM и память#Многопоточность и асинхронность

Комментарии (3)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

🔍 Может ли сборщик мусора собрать "висящую" подписку?

Нет, сборщик мусора не может автоматически собрать "висящую" (неотмененную) подписку, если на неё остаётся активная ссылка. Это классическая проблема управления жизненным циклом в Android и типичный источник утечек памяти (memory leaks).

🧠 Принцип работы

Сборщик мусора (Garbage Collector, GC) в Java/Kotlin освобождает память, занятую объектами, на которые больше нет "живых" (достижимых) ссылок из корней GC (например, активных потоков, статических полей, локальных переменных в стеке). "Висящая" подписка — это ситуация, когда объект, создающий подписку (например, Activity, Fragment или ViewModel), уничтожается, но подписка продолжает существовать, удерживая ссылку на этот объект, препятствуя его сборке. Это создаёт циклическую или сильную ссылку.

💡 Пример утечки с RxJava / Coroutines

Рассмотрим пример с RxJava:

class MyActivity : AppCompatActivity() {

    private val apiService = ApiService()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // ПОДПИСКА БЕЗ ОТМЕНЫ - ИСТОЧНИК УТЕЧКИ!
        apiService.fetchData()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { data ->
                // Обновляем UI, который принадлежит уничтоженной Activity
                updateUI(data) // Вызовется после onDestroy() -> Crash!
            }
        // disposable не сохранён, не отменён в onDestroy()
    }
    
    private fun updateUI(data: Data) {
        textView.text = data.text // textView уже может быть null!
    }
}

После поворота экрана или закрытия MyActivity:

  1. Activity должна быть уничтожена.
  2. Но подписка продолжает работать в фоне, так как Disposable не отменён.
  3. Подписка содержит неявную ссылку на внешний класс (MyActivity.this) через лямбду.
  4. MyActivity остаётся в памяти, пока работает подписка → утечка памяти.
  5. При получении данных и попытке обновить textView (который уже null) — краш приложения.

✅ Правильный подход: Отмена подписок

Для предотвращения утечек необходимо явно управлять жизненным циклом подписок:

Решение для RxJava: CompositeDisposable

class MyActivity : AppCompatActivity() {

    private val apiService = ApiService()
    private val disposables = CompositeDisposable() // Контейнер для подписок

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val disposable = apiService.fetchData()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { data ->
                updateUI(data)
            }
            
        disposables.add(disposable) // Добавляем для последующей отмены
    }
    
    override fun onDestroy() {
        super.onDestroy()
        disposables.clear() // ОТМЕНА ВСЕХ ПОДПИСОК при уничтожении Activity
    }
}

Решение для Kotlin Coroutines: Job / CoroutineScope

class MyActivity : AppCompatActivity() {

    private val apiService = ApiService()
    private var job: Job? = null // Ссылка на запущенную корутину

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Запуск в жизненном цикле Activity
        job = lifecycleScope.launch { // Используем встроенный lifecycleScope
            val data = apiService.fetchData() // suspend-функция
            updateUI(data)
        }
        // lifecycleScope автоматически отменит корутину в onDestroy()
    }
    
    // Явная отмена, если не используем lifecycleScope
    override fun onDestroy() {
        super.onDestroy()
        job?.cancel() // Явная отмена корутины
    }
}

🛡️ Проактивные стратегии предотвращения

  1. Использование lifecycleScope (для корутин) и autoDispose (для RxJava) — автоматическая отмена при разрушении жизненного цикла.
  2. Использование ViewModel + viewModelScope — подписки живут только пока жива ViewModel (переживают поворот экрана, но очищаются при окончательном уничтожении).
  3. Слабые ссылки (WeakReference) — в некоторых случаях, но это антипаттерн для подписок, лучше явное управление жизненным циклом.
  4. Статические анализаторы кода (например, LeakCanary) для обнаружения утечек в режиме разработки.

📚 Итог

  • Сборщик мусора бессилен против "висящих" подписок, так как они сами удерживают ссылки на уничтоженные объекты.
  • Ответственность за отмену подписок лежит на разработчике через механизмы Disposable.dispose(), CompositeDisposable, Job.cancel() или автоматизированные lifecycleScope/viewModelScope.
  • Игнорирование этого правила ведёт к утечкам памяти, повышенному потреблению CPU/батареи и крашам приложения. Всегда связывайте асинхронные операции с жизненным циклом компонентов Android.