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

Соберет ли Garbage Collector циклические ссылки

1.8 Middle🔥 183 комментариев
#Dependency Injection#Архитектура и паттерны

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

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

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

Краткий ответ

Да, Garbage Collector (GC) в Android/Java соберет объекты с циклическими ссылками, если на них нет достижимых ссылок извне этой цикла. Это основное отличие от систем, использующих подсчет ссылок (reference counting).

Подробное объяснение

Основной механизм современного GC в Android (начиная с ART) и Java (JVM) — это достижимость (reachability). Сборщик мусора начинает свой обход с так называемых GC Roots (корневых объектов):

GC Roots (Корни сборки мусора)

  • Активные потоки (Thread objects).
  • Статические переменные (static fields).
  • Локальные переменные в стеке вызовов метода (т.е., в стеке потока).
  • JNI-ссылки в нативном коде.
  • Системные классы, загруженные системным загрузчиком классов (например, java.lang.String).

Сборщик помечает как живые (live) все объекты, до которых можно добраться, стартуя от этих корней через цепочки ссылок (поля объектов). Все остальные объекты считаются недостижимыми (unreachable) и подлежат удалению, независимо от того, ссылаются ли они друг на друга.

Пример с циклической ссылкой

class Node {
    Node next;

    public Node(Node next) {
        this.next = next;
    }
}

// Пример создания и "потери" циклической ссылки
void createCycleAndLoseIt() {
    Node nodeA = new Node(null);
    Node nodeB = new Node(nodeA);
    nodeA.next = nodeB; // Создаем цикл: A -> B -> A -> B ...

    // После выхода из метода локальные переменные nodeA и nodeB удаляются из стека.
    // Теперь на цикл (A и B) нет ссылок из GC Roots.
    // Несмотря на цикл, оба объекта НЕДОСТИЖИМЫ и будут собраны GC.
}

В момент, когда функция завершается, локальные переменные nodeA и nodeB (которые были в стеке потока и являлись корнями) исчезают. Теперь ни на nodeA, ни на nodeB не указывает ни один корень GC. Цикл существует, но он "висит в пустоте". Сборщик мусора, начиная обход от корней, не найдет путь к этим объектам и смело пометит их как мусор.

Сравнение с подсчетом ссылок (Reference Counting)

Важно понимать, почему этот вопрос так часто задают. В системах с подсчетом ссылок (используется, например, в старом Objective-C до ARC, в некоторых сценариях CPython) циклические ссылки — это классическая проблема утечки памяти (memory leak).

# Пример проблемы цикла при подсчете ссылок (Python)
class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b  # Счетчик ссылок на b = 2
b.ref = a  # Счетчик ссылок на a = 2

# Даже если удалить внешние ссылки, счетчики станут a=1, b=1 (из-за цикла).
# Ни один объект не достигнет 0, и память никогда не освободится (утечка).
del a
del b

В Java и Android такой проблемы нет благодаря алгоритмам, основанным на достижимости.

Когда циклические ссылки могут стать проблемой?

Проблема возникает не из-за самого цикла, а если на него остается неявная или ошибочная ссылка из GC Root. Это уже классическая утечка памяти (memory leak), и цикл лишь усложняет ее обнаружение.

Типичные сценарии утечек с циклами в Android:

  1. Неотписанные слушатели (Listeners) и обратные вызовы (Callbacks):

    class LeakyActivity : AppCompatActivity() {
        private val heavyObject = HeavyObject()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // HeavyObject держит ссылку на слушатель, который является нестатическим
            // внутренним классом и неявно держит ссылку на внешний Activity.
            heavyObject.setListener(object : HeavyObject.Listener {
                override fun onEvent() {
                    // Используем метод Activity
                    updateUI()
                }
            })
        }
        // Активность уничтожена, но heavyObject где-то жив,
        // а его listener держит ссылку на мертвую активность -> УТЕЧКА.
    }
    
  2. Статические поля, хранящие контекст или View:

    public class AppSingleton {
        // СТАТИЧЕСКОЕ поле держит ссылку на контекст активности -> утечка.
        public static Context sActivityContext;
    
        // Некорректная сильная ссылка на View
        public static TextView sBadTextView;
    }
    

Заключение

  • Основной принцип: Garbage Collector в Android/Java удаляет объекты, основываясь на их достижимости из GC Roots, а не на отсутствии ссылок вообще. Поэтому изолированные циклы (недостижимые) собираются.
  • Главный вывод для разработчика: Вам не нужно вручную разрывать циклические ссылки для помощи GC (как в некоторых других языках). Ваша задача — следить, чтобы на объекты, которые должны быть собраны, не оставалось неявных сильных ссылок из "живых" корней (статических полей, неотписанных колбэков, фоновых потоков и т.д.).
  • Инструменты: Для поиска именно таких утечек, когда цикл удерживается извне, используйте Android Studio Profiler (Memory), LeakCanary или Android Memory Hprof Analyzer. Они отлично показывают цепочки ссылок от GC Root до утекающего объекта.