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

Как сборщик мусора проверяет, что переменная используется

2.0 Middle🔥 191 комментариев
#JVM и управление памятью#Многопоточность#Основы Java

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

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

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

Как сборщик мусора проверяет, что переменная используется

Сборщик мусора (Garbage Collector) в Java определяет, какие объекты в памяти больше не используются и удаляет их. Это автоматический процесс, критичный для управления памятью. Основной механизм — анализ достижимости объектов через граф ссылок.

Концепция: Reachability (достижимость)

Объект считается достижимым (reachable), если его можно получить, следуя цепочке ссылок из корневых объектов (GC roots). Если объект не достижим — это мусор.

public class Example {
    public static void main(String[] args) {
        User user1 = new User("John");    // Переменная user1 - корень
        User user2 = user1;                // Ссылка на тот же объект
        
        user1 = null;                      // user1 больше не указывает на объект
        // Но объект ЕЩЁ достижим через user2
        
        user2 = null;                      // user2 тоже null
        // Теперь объект НЕ ДОСТИЖИМ, GC может удалить
    }
}

Корневые объекты (GC Roots)

GC отслеживает ссылки, начиная с этих корневых объектов:

1. LOCAL VARIABLES
   ├─ Переменные в текущем методе
   └─ Переменные в стеке вызовов

2. ACTIVE THREADS
   ├─ Каждый активный поток
   └─ Переменные в его стеке

3. STATIC REFERENCES
   ├─ Статические переменные класса
   └─ Глобальные объекты

4. JNI REFERENCES
   ├─ Ссылки из Java Native Interface
   └─ Объекты, переданные в native код

Алгоритм: Mark and Sweep

Это самый популярный алгоритм сборки мусора:

Шаг 1: MARK (маркировка)
┌──────────────────────────────────────────┐
│ GC root (переменная main)                │
│         │                                 │
│         ▼                                 │
│  user ──→ User{name="John"}  ✓ MARKED   │
│         │                                 │
│         └──→ Address{city="NYC"} ✓ MARKED│
└──────────────────────────────────────────┘

Объекты, на которые НЕ указывают из корней:
- OldUser{"Bob"} ✗ NOT MARKED (мусор)
- OldAddress{} ✗ NOT MARKED (мусор)

Шаг 2: SWEEP (очистка)
┌──────────────────────────────────────────┐
│ Удалить все объекты, которые НЕ маркированы
│                                           │
│ Памятью удаляются: OldUser, OldAddress  │
└──────────────────────────────────────────┘

Пример: как GC отслеживает ссылки

public class GarbageCollectionExample {
    public static void main(String[] args) {
        // Фаза 1: создание объектов
        User user = new User("Alice");  // GC корень: переменная 'user'
        Address addr = new Address("NYC");  // GC корень: переменная 'addr'
        user.setAddress(addr);  // Ссылка через поле
        
        // Фаза 2: "отпущение" адреса
        addr = null;  // Переменная addr больше не указывает на Address
        // НО Address ЕЩЁ достижим через user.address
        
        // Фаза 3: "отпущение" пользователя
        user = null;  // Теперь user не указывает на User
        // Address ТАКЖЕ становится недостижима (удаляется вместе с User)
    }
}

// Graph ссылок:
// Начально:
// GC roots:
//   user → User{address → Address{...}}
//   addr ↗

// После addr = null:
// GC roots:
//   user → User{address → Address{...}}

// После user = null:
// GC roots: (ничего)
// ВСЕ объекты могут быть удалены

Отслеживание в разных типах памяти

Heap (кучу, где живут объекты)

public class HeapTracking {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();  // list - GC root (в стеке)
        list.add("hello");  // "hello" объект в heap, достижим через list
        list.add("world");  // "world" объект в heap, достижим через list
        
        list = null;  // list больше не указывает на ArrayList
        // Оба String объекта и ArrayList БОЛЬШЕ не достижимы
    }
}

// Memory layout:
// STACK:  | list | ──┐
//         └──────┘   │
// HEAP:              ▼
//      [ArrayList]──→[String "hello"]
//              │──→[String "world"]
//
// После list = null:
// STACK:  | list=null |
// HEAP:   (all objects unreachable)

Stack (где хранятся ссылки локальных переменных)

public void methodA() {
    User user = new User();  // Создать объект в heap, ссылка в stack
    methodB(user);  // Передать ссылку
    // После return из methodB, локальная переменная user ещё в стеке
    // Объект в heap ещё достижим
}

public void methodB(User user) {
    methodC(user);  // Передать ссылку
    // user из methodB ещё достижим
}

public void methodC(User user) {
    // Самая глубокая ссылка
    // user в стеке: [main → methodA → methodB → methodC]
}

// Stack после каждого return:
// methodC() вернулась: [main → methodA → methodB]
// methodB() вернулась: [main → methodA]
// methodA() вернулась: [main]
// Если больше нет ссылок — объект удаляется

Определение "используется ли переменная"

GC не смотрит, будет ли переменная использована в БУДУЩЕМ. Он смотрит, ДОСТИЖИМА ли она СЕЙЧАС:

public void example() {
    Object obj = new Object();  // Объект создан, достижим
    
    obj = null;  // Ссылка обнулена
    // Объект БОЛЬШЕ не достижим, даже если дальше идёт код
    
    System.out.println("Объект был удалён GC");
    // Это просто не использует obj, но obj уже не существует
}

Граф ссылок в памяти

public class ReferenceGraphExample {
    public static void main(String[] args) {
        // Граф объектов:
        User user = new User("Bob");  // GC root: user
        Address addr = new Address();  // GC root: addr
        Company company = new Company();  // GC root: company
        
        user.setAddress(addr);  // user → addr
        company.setManager(user);  // company → user
        addr.setCountry(country);  // addr → country
        
        // Достижимые объекты:
        // user (прямой GC root)
        // addr (через user)
        // company (прямой GC root)
        // country (через addr)
    }
}

// Граф памяти:
//  GC Roots:
//  ┌─ user ──→ User ────→ Address ──→ Country
//  │             │
//  │             └──→ name (String)
//  │
//  ├─ addr ────→ Address ──→ Country ✓ ДОСТИЖИМ (уже маркирован)
//  │
//  └─ company ──→ Company ──→ Manager (User) ✓ ДОСТИЖИМ
//
//  Недостижимые объекты:
//  OldCompany (никто на неё не указывает) ✗

Специальные случаи: Weak References

import java.lang.ref.*;

public class ReferenceTypes {
    public static void main(String[] args) {
        // 1. Strong reference (обычная ссылка)
        Object strong = new Object();  // ДОСТИЖИМА, GC не удалит
        
        // 2. Weak reference (слабая ссылка)
        WeakReference<Object> weak = new WeakReference<>(new Object());
        Object obj = weak.get();  // Может вернуть null если GC удалил
        // Слабая ссылка НЕ препятствует сборке мусора
        
        // 3. Soft reference (мягкая ссылка)
        SoftReference<byte[]> cache = new SoftReference<>(new byte[1024]);
        // GC удалит, только если памяти критически не хватает
        
        // 4. Phantom reference (фантомная ссылка)
        PhantomReference<Object> phantom = 
            new PhantomReference<>(new Object(), queue);
        // Используется только для cleanup
    }
}

Как объекты помечаются во время GC

До GC:
Heap: [Object1]✗ [Object2]✓ [Object3]✗ [Object4]✓ ...

Время выполнения GC:
1. Найти GC roots (переменные, потоки, статические поля)
2. Следовать по ссылкам и маркировать достижимые объекты
3. Удалить немаркированные объекты

После GC:
Heap: [Object2]✓ [Object4]✓ ... [пусто] [пусто] ...
      ↑                          ↑
   Живые объекты         Освобождённая память

Оптимизация: Escape Analysis

Модернные JVM (HotSpot, GraalVM) используют анализ для оптимизации:

public int calculate() {
    Point p = new Point(0, 0);  // Объект НЕ выходит из метода
    p.x = 10;
    return p.x;
    // HotSpot может ИСКЛЮЧИТЬ создание объекта в памяти
    // и работать с переменными прямо в стеке
}

// Оптимизировано эквивалентно:
public int calculate() {
    // Object allocation eliminated
    int x = 10;
    return x;
}

Визуализация работы GC

public class GCVisualization {
    public static void main(String[] args) {
        // Создание объектов
        for (int i = 0; i < 5; i++) {
            Object obj = new Object();  // Объект создан
            System.out.println("Created " + i);
        }  // Конец блока - ссылка obj выходит из области видимости
        // GC может удалить эти объекты
        
        // Явный запрос GC (не гарантирует выполнение)
        System.gc();
    }
}

// Что происходит:
// i=0: Create Object → obj references it → exit loop body → obj unreachable
// i=1: Create Object → obj references it → exit loop body → obj unreachable
// i=2: Create Object → obj references it → exit loop body → obj unreachable
// i=3: Create Object → obj references it → exit loop body → obj unreachable
// i=4: Create Object → obj references it → exit loop body → obj unreachable
// System.gc(): Mark objects as garbage, sweep them

Резюме

  • GC определяет использование через достижимость: объект используется, если на него можно указать ссылку из GC roots
  • GC roots: локальные переменные, активные потоки, статические поля, JNI ссылки
  • Mark and Sweep алгоритм: маркировать достижимые объекты, удалить остальные
  • Переменная считается неиспользованной, когда все ссылки на её объект удалены
  • Weak references не препятствуют сборке мусора
  • GC работает автоматически, но можно оптимизировать через Escape Analysis и правильное управление ссылками
Как сборщик мусора проверяет, что переменная используется | PrepBro