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

Как Garbage Collector работает с циклической зависимостью

2.4 Senior🔥 151 комментариев
#JVM и управление памятью

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

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

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

Ответ: Как Garbage Collector работает с циклической зависимостью?

Циклическая зависимость (circular reference) — это ситуация, когда два или более объекта ссылаются друг на друга, создавая замкнутый цикл. Это серьёзная проблема в старых языках (C, C++), но Java GC справляется с ней эффективно.

Пример циклической зависимости:

public class Person {
    private String name;
    private Person spouse;  // может ссылаться на другого Person
    
    public void setSpouse(Person spouse) {
        this.spouse = spouse;
    }
}

// Использование
Person alice = new Person("Alice");
Person bob = new Person("Bob");

alice.setSpouse(bob);     // alice -> bob
bob.setSpouse(alice);     // bob -> alice

// Теперь: alice <-> bob (циклическая зависимость)

Если удалить ссылки на alice и bob:

alice = null;
bob = null;

// Но объекты всё ещё ссылаются друг на друга!
// alice.spouse = bob, bob.spouse = alice

Как GC находит мусор?

Java использует алгоритм Reachability Analysis (анализ достижимости):

1. GC Roots (корни GC)

GC начинает поиск с известных корней:

  • Локальные переменные в стеке
  • Статические переменные классов
  • Объекты в очередях (queue) для обработки
public class GCRootsExample {
    public static Object staticRef = new Object(); // GC Root
    
    public void example() {
        Object local = new Object();  // GC Root (в стеке)
        // ...
    }
}

2. Mark-Sweep алгоритм

GC проходит от корней и отмечает все достижимые объекты:

Шаг 1 - Mark (отметить достижимое):
  GC Root
    ↓
  Object A ← отмечено как живое
    ↓
  Object B ← отмечено как живое
    ↓
  Object C ← отмечено как живое

Шаг 2 - Sweep (удалить недостижимое):
  Объект D ← не достижим из root, удаляется
  Объект E ← не достижим из root, удаляется

Циклические зависимости и GC:

Сценарий 1: Циклический мусор

public class Node {
    public Node next;
    public int data;
}

// Создаём циклический список
Node node1 = new Node();
Node node2 = new Node();
node1.next = node2;  // node1 -> node2
node2.next = node1;  // node2 -> node1

// Теперь node1 и node2 недостижимы из GC roots
node1 = null;
node2 = null;

Что происходит:

До GC:
  GC Root: node1 = null, node2 = null
  Объекты Node1 и Node2 в heap:
    Node1 <-> Node2 (циклическая ссылка)

Время запуска GC (Mark phase):
  - GC root -> null, не указывает на живое
  - Node1 не достижим из root
  - Node2 не достижим из root
  - Оба объекта не отмечены как живые

Sweep phase:
  - Node1 удаляется
  - Node2 удаляется
  - Цикл разрывается, память освобождается

Ключевой момент: Java GC находит мусор по достижимости из GC roots, НЕ по количеству ссылок на объект. Поэтому циклические ссылки не являются проблемой!

Это отличается от Reference Counting:

// В языках с Reference Counting (Python - раньше):
Node1 refCount = 2  (на него указывают: Node2, переменная node1)
Node2 refCount = 2  (на него указывают: Node1, переменная node2)

После: node1 = null, node2 = null
Node1 refCount = 1  (на него указывает: Node2)
Node2 refCount = 1  (на него указывает: Node1)

// refCount никогда не станет 0!
// Мусор НИКОГДА не будет собран
// Утечка памяти!

// Java не использует Reference Counting (кроме некоторых случаев)

Практический пример с памятью:

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;

public class GCCyclicTest {
    static class Node {
        Node next;
        byte[] data = new byte[1024 * 1024]; // 1 MB данных
    }
    
    public static void main(String[] args) throws Exception {
        MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
        
        System.out.println("Memory before: " + memory.getHeapMemoryUsage());
        
        // Создаём циклический граф
        Node node1 = new Node();
        Node node2 = new Node();
        Node node3 = new Node();
        
        node1.next = node2;  // node1 -> node2
        node2.next = node3;  // node2 -> node3
        node3.next = node1;  // node3 -> node1 (ЦИКЛ!)
        
        System.out.println("Memory after creation: " + memory.getHeapMemoryUsage());
        
        // Разрываем все ссылки из root
        node1 = null;
        node2 = null;
        node3 = null;
        
        System.out.println("Memory after nullify (before GC): " + memory.getHeapMemoryUsage());
        
        // Запускаем GC
        System.gc();
        Thread.sleep(100);
        
        System.out.println("Memory after GC: " + memory.getHeapMemoryUsage());
        // Память освобождена, несмотря на циклические ссылки!
    }
}

Визуализация Mark-Sweep для циклов:

До GC:
  [GC Roots]
    |
    v
  null (node1, node2, node3)
    
  [Heap]
  Node1 (1MB) <--+
    |            |
    v            |
  Node2 (1MB)    |
    |            |
    v            |
  Node3 (1MB)----+
  
  Node1, Node2, Node3 — не достижимы из root!

Во время Mark фазы:
  GC начинает с root -> null
  Ничего не отмечается
  
During Sweep фазе:
  Node1, Node2, Node3 удаляются
  Циклические ссылки разрываются
  Память освобождается (3 MB)

WeakReference для циклических зависимостей:

Если нужно допустить циклические ссылки без утечек, можно использовать WeakReference:

public class Node {
    public Node next;
    private WeakReference<Node> previousRef;
    
    public void setPrevious(Node previous) {
        this.previousRef = new WeakReference<>(previous);
    }
    
    public Node getPrevious() {
        return previousRef != null ? previousRef.get() : null;
    }
}

// Использование
Node first = new Node();
Node second = new Node();

first.next = second;              // сильная ссылка
second.setPrevious(first);         // слабая ссылка

// Если нужна только first
first = null;

// GC может удалить second, даже если он имеет слабую ссылку на first

Типы references в Java:

// 1. Strong Reference (сильная) — по умолчанию
Object obj = new Object();

// 2. Weak Reference — может быть собрана GC
WeakReference<Object> weak = new WeakReference<>(obj);

// 3. Soft Reference — собирается при нехватке памяти
SoftReference<Object> soft = new SoftReference<>(obj);

// 4. Phantom Reference — используется для cleanup
PhantomReference<Object> phantom = new PhantomReference<>(obj, queue);

Заключение:

  1. Java GC использует Reachability Analysis, а не Reference Counting
  2. Циклические зависимости НЕ являются проблемой в Java
  3. Мусор собирается по достижимости из root, не по количеству ссылок
  4. WeakReference используется для допуска циклических ссылок при необходимости
  5. Утечки памяти происходят только когда объекты остаются достижимыми из root

Это одно из больших преимуществ Java перед C/C++, где циклические ссылки требуют ручного управления или сложного подсчёта ссылок.

Как Garbage Collector работает с циклической зависимостью | PrepBro