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

Как Garbage collector понимает, что объект можно уничтожить

1.7 Middle🔥 181 комментариев
#Основы Java

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

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

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

# Как Garbage Collector понимает, что объект можно уничтожить

Основной механизм: Reachability Analysis

Гарбейж коллектор определяет, что объект можно удалить через анализ достижимости (reachability analysis):

  1. Объект можно удалить, если он недостижим из GC root'ов
  2. GC root — это объекты, которые JVM сохраняет всегда
  3. Если нет цепи ссылок из root'ов к объекту — объект удалён

GC Root'ы

Это точки входа для анализа:

1. Локальные переменные текущих потоков
   └── int x; String s; List<Order> orders;

2. Статические переменные классов
   └── public static final Logger LOG;
   └── public static Configuration config;

3. JNI ссылки (из нативного кода C++)
   └── jobject obj;

4. Method area (метаданные ClassLoader'ов)
   └── Информация о классах

5. Thread stack — стек каждого потока
   └── Все локальные переменные

Mark-and-Sweep алгоритм

Гарбейж коллектор работает в 2 фазы:

Фаза 1: MARK (Пометь)

1. Начните с GC root'ов
2. Используйте DFS (Depth-First Search) для обхода
3. Помечаем каждый достижимый объект

Пример:

Root (Stack):
  ├─ list (ArrayList)     ← помечаем
  │  └─ item1 (String)    ← помечаем
  │  └─ item2 (String)    ← помечаем
  │
  ├─ config (Config)      ← помечаем
  │  └─ database (DB)     ← помечаем
  │
  └─ unused (Data)        ← НЕ помечаем
     └─ temp (String)     ← НЕ помечаем

Фаза 2: SWEEP (Подмести)

1. Пройти по всем объектам на heap
2. Если объект не помечен → удалить
3. Освободить память

Результат:

До SWEEP:           После SWEEP:
┌──────────┐       ┌──────────┐
│ item1    │ ✓     │ item1    │
├──────────┤       ├──────────┤
│ item2    │ ✓     │ item2    │
├──────────┤       ├──────────┤
│ database │ ✓     │ database │
├──────────┤       ├──────────┤
│ unused   │ ✗  →  │ [empty]  │
├──────────┤       ├──────────┤
│ temp     │ ✗  →  │ [empty]  │
└──────────┘       └──────────┘

Пример: Как GC определяет удаление

public class GCDemo {
    public static void main(String[] args) {
        // === Этап 1: Создание объектов ===
        
        // Strong reference
        List<String> list = new ArrayList<>();
        list.add("data1");
        list.add("data2");
        // ✅ ArrayList и String достижимы через root list
        
        Map<String, Object> config = new HashMap<>();
        config.put("key", "value");
        // ✅ HashMap и String достижимы через root config
        
        // Бесполезный объект
        String unused = "this will be gc'd";
        // ✅ String достижим через root unused
        
        // === Этап 2: Удаляем ссылки ===
        
        unused = null;
        // ❌ String больше не достижим
        // GC может удалить на следующем цикле
        
        list = null;
        // ❌ ArrayList и его содержимое больше не достижимы
        
        config = null;
        // ❌ HashMap и его содержимое больше не достижимы
        
        // === Этап 3: Запуск GC ===
        System.gc();  // Просьба к JVM запустить GC
        
        // GC выполнит:
        // 1. MARK: помечает объекты, которые ещё достижимы
        // 2. SWEEP: удаляет непомеченые (unused, list, config)
    }
}

Проблема: Circular Reference (Циклическая ссылка)

class Node {
    Node next;
    byte[] data = new byte[1024];
}

public class CircularDemo {
    public static void main(String[] args) {
        Node node1 = new Node();
        Node node2 = new Node();
        
        // Создали циклическую ссылку
        node1.next = node2;
        node2.next = node1;  // node1 ← node2 ← node1
        
        // Удаляем strong reference
        node1 = null;
        node2 = null;
        
        // ❌ node1 и node2 ссылаются друг на друга!
        // Но оба недостижимы из root'ов
        
        System.gc();
        // ✅ GC удалит обоих
        // (GC способен ловить циклические ссылки через graph tracing)
    }
}

GC Roots vs Обычные объекты

public class RootsDemo {
    // === GC ROOTS ===
    
    // 1. Статические переменные — это root!
    static List<String> staticList = new ArrayList<>();
    
    public static void main(String[] args) {
        // 2. Локальные переменные main — root!
        String localVar = "I am root";
        List<Order> orders = new ArrayList<>();
        
        // Даже если orders = null в конце main,
        // эта переменная была root, пока main выполнялась
    }
    
    // === ОБЫЧНЫЕ ОБЪЕКТЫ ===
    
    public void someMethod() {
        // Локальная переменная — root только внутри этого метода
        String temp = "temporary";
        // Когда someMethod() вернёт управление,
        // temp уже не является root
    }
}

Виды GC Collectors

Разные GC алгоритмы, но все используют reachability:

G1GC (Garbage First)
├── Делит heap на регионы
├── Помечает регионы с мусором
└── Собирает регионы с наибольшей плотностью мусора

ZGC (Zero GC)
├── Очень быстрый (pause < 10ms)
├── Использует цветные указатели
└── Помечает во время выполнения программы

Shenandoah
├── Низкие паузы
├── Concurrent marking и compaction
└── Работает параллельно с приложением

Как отследить удаление объектов

public class GCTracking {
    public static void main(String[] args) throws Exception {
        // Создаём объект со своим finalize
        class Traceable {
            String name;
            
            Traceable(String name) {
                this.name = name;
            }
            
            @Override
            protected void finalize() throws Throwable {
                System.out.println("Garbage collecting: " + name);
                super.finalize();
            }
        }
        
        Traceable obj1 = new Traceable("obj1");
        Traceable obj2 = new Traceable("obj2");
        
        obj1 = null;  // obj1 становится garbage
        
        System.gc();
        Thread.sleep(100);  // Ждём finalization
        // Вывод: "Garbage collecting: obj1"
        
        obj2 = null;  // obj2 становится garbage
        
        System.gc();
        Thread.sleep(100);
        // Вывод: "Garbage collecting: obj2"
    }
}

Важно: finalize() deprecated и больше не используется. Используй try-with-resources вместо.

На собеседовании

"Garbage Collector понимает, что объект можно уничтожить через анализ достижимости (reachability analysis):

  1. Начинает с GC root'ов (локальные переменные потоков, статики)
  2. Обходит граф объектов (DFS) и помечает все достижимые объекты
  3. Все непомеченые объекты считаются мусором
  4. Удаляет мусор и освобождает память

Это называется Mark-and-Sweep алгоритм.

Пример: если переменная = null, объект становится недостижим и будет удалён на следующем GC цикле.

Даже циклические ссылки удаляются, потому что GC смотрит на граф в целом, а не на отдельные ссылки."