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

Могут ли циклические ссылки создать утечку памяти

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

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

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

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

# Циклические ссылки и утечки памяти в Java

Циклические ссылки — ситуация, когда объекты ссылаются друг на друга, образуя цикл. Это интересный вопрос, потому что ответ зависит от типа сборщика мусора (GC).

1. Как создаются циклические ссылки

public class Node {
    private String value;
    private Node next;
    private Node prev;
    
    public Node(String value) {
        this.value = value;
    }
}

public class CircularReferenceExample {
    public static void main(String[] args) {
        // Создаём циклические ссылки
        Node node1 = new Node("A");
        Node node2 = new Node("B");
        Node node3 = new Node("C");
        
        // Связываем в цикл
        node1.next = node2;
        node2.next = node3;
        node3.next = node1;  // циклическая ссылка!
        
        node1.prev = node3;
        node2.prev = node1;
        node3.prev = node2;
        
        // Удаляем все внешние ссылки
        node1 = null;
        node2 = null;
        node3 = null;
        
        // Теперь объекты недостижимы снаружи, но ссылаются друг на друга
        // Это может стать утечкой памяти
    }
}

2. Были ли утечки памяти в старых версиях Java?

2.1 Java 1.0-1.3 (Reference Counting)

// В старых версиях использовался Reference Counting
// Каждый объект имел счётчик ссылок

// Проблема: циклические ссылки НЕ удаляются
public class OldJavaLeakExample {
    static class Node {
        Node next;
    }
    
    public static void main(String[] args) {
        Node a = new Node();
        Node b = new Node();
        
        a.next = b;
        b.next = a;  // циклическая ссылка
        
        // Reference counting:
        // a: refCount = 2 (переменная a + ссылка от b)
        // b: refCount = 2 (переменная b + ссылка от a)
        
        a = null;  // refCount(a) = 1
        b = null;  // refCount(b) = 1
        
        // Оба объекта остаются в памяти ВЕЧНО!
        // Это УТЕЧКА ПАМЯТИ в Java 1.0-1.3
    }
}

3. Современная Java и GC (Garbage Collection)

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

// Современная Java использует Mark-and-Sweep
// Это находит циклические ссылки

public class ModernJavaNoLeak {
    static class Node {
        String value;
        Node next;
        
        Node(String value) {
            this.value = value;
        }
    }
    
    public static void main(String[] args) {
        Node node1 = new Node("A");
        Node node2 = new Node("B");
        
        node1.next = node2;
        node2.next = node1;  // циклическая ссылка
        
        // Процесс GC (Mark and Sweep):
        // 1. Mark фаза: начинаем от GC root (стека)
        //    - node1 помечена
        //    - node2 помечена (через node1.next)
        // 2. Sweep фаза: удаляем немеченые объекты
        
        node1 = null;
        node2 = null;
        
        // Теперь нет ссылок из GC root
        // GC не помечает node1 и node2
        // Они удаляются несмотря на циклические ссылки!
        // НЕТ утечки памяти
        
        // Вызовем GC (добровольно, для примера)
        System.gc();
        System.out.println("Циклические ссылки удалены");
    }
}

4. Недостижимость (Reachability)

public class ReachabilityExample {
    static class Node {
        Node next;
        int data;
    }
    
    public static void main(String[] args) {
        Node root = new Node();
        Node a = new Node();
        Node b = new Node();
        
        root.next = a;
        a.next = b;
        b.next = a;  // циклическая ссылка между a и b
        
        // Graph of references:
        // GC root -> root -> a -> b
        //                    ^    |
        //                    +----+  (цикл)
        
        // Все объекты ДОСТИЖИМЫ от GC root
        // Они НЕ будут удалены
        
        // Только удаление root приводит к удалению всех
        root = null;
        
        // Теперь:
        // - root недостижима
        // - a недостижима (не достичь от root)
        // - b недостижима (не достичь от root)
        // Несмотря на циклические ссылки между a и b!
        
        // GC удалит все три объекта
    }
}

5. Реальные утечки памяти в Java

5.1 Static References (реальная утечка)

public class StaticLeakExample {
    
    static class Node {
        int[] data = new int[1024 * 1024];  // 4MB
        Node next;
    }
    
    // Статическая ссылка — ВЕЧНО в памяти
    static Node HEAD = new Node();
    
    public static void main(String[] args) {
        Node a = new Node();
        Node b = new Node();
        
        a.next = b;
        b.next = a;  // циклическая ссылка
        
        HEAD.next = a;  // Привязали к static переменной
        
        a = null;
        b = null;
        
        // a и b НЕ удалятся, потому что HEAD ссылается на них
        // Это РЕАЛЬНАЯ утечка памяти
        
        // GC не может удалить, пока приложение работает
    }
}

5.2 Event Listeners (частая утечка)

public class ListenerLeakExample {
    
    static class Button {
        List<ClickListener> listeners = new ArrayList<>();
        
        public void onClick(ClickListener listener) {
            listeners.add(listener);
        }
        
        public void click() {
            for (ClickListener listener : listeners) {
                listener.onButtonClicked();
            }
        }
    }
    
    interface ClickListener {
        void onButtonClicked();
    }
    
    static class Dialog {
        Button button;
        Button cancelButton;
        
        public Dialog(Button button) {
            this.button = button;
            
            // Анонимный класс содержит ссылку на this (Dialog)
            button.onClick(new ClickListener() {
                @Override
                public void onButtonClicked() {
                    System.out.println("Dialog: " + Dialog.this.hashCode());
                }
            });
            
            // Button -> ClickListener -> Dialog
            // Если Dialog удаляется, listener НЕ удаляется
            // потому что Button всё ещё его содержит
        }
    }
    
    public static void main(String[] args) {
        Button button = new Button();
        Dialog dialog = new Dialog(button);
        
        dialog = null;  // Dialog удалится?
        
        // НЕТ! Потому что button.listeners содержит ссылку
        // на анонимный класс, который ссылается на dialog
        // Это циклическая ссылка: button -> listener -> dialog
        // и dialog не удалится
        
        // Решение: явно удалить listener
        // button.listeners.clear();
    }
}

6. Как избежать утечек памяти

6.1 Использование WeakReference

import java.lang.ref.WeakReference;

public class WeakReferenceExample {
    
    static class CacheEntry {
        int id;
        byte[] data;
        
        CacheEntry(int id) {
            this.id = id;
            this.data = new byte[1024 * 1024];  // 1MB
        }
    }
    
    static class Cache {
        // WeakReference позволяет GC удалить объект
        Map<Integer, WeakReference<CacheEntry>> cache = new HashMap<>();
        
        public void put(int id, CacheEntry entry) {
            cache.put(id, new WeakReference<>(entry));
        }
        
        public CacheEntry get(int id) {
            WeakReference<CacheEntry> ref = cache.get(id);
            
            if (ref == null) {
                return null;
            }
            
            CacheEntry entry = ref.get();  // может быть null
            if (entry == null) {
                cache.remove(id);  // уже удалён GC
            }
            return entry;
        }
    }
}

6.2 Явное удаление ссылок

public class ExplicitCleanup {
    
    static class Resource {
        int[] data = new int[1024 * 1024];
        
        public void cleanup() {
            data = null;  // явно освобождаем память
        }
    }
    
    static class Manager {
        List<Resource> resources = new ArrayList<>();
        
        public void clear() {
            for (Resource r : resources) {
                r.cleanup();  // очищаем ресурсы
            }
            resources.clear();  // очищаем список
        }
    }
    
    public static void main(String[] args) {
        Manager manager = new Manager();
        
        for (int i = 0; i < 100; i++) {
            manager.resources.add(new Resource());
        }
        
        // Используем ресурсы...
        
        manager.clear();  // явно очищаем
        manager = null;
    }
}

6.3 Try-with-Resources

public class TryWithResourcesExample {
    
    static class Connection implements AutoCloseable {
        Connection next;
        
        @Override
        public void close() {
            // Очищаем ссылку на следующую связь
            next = null;
        }
    }
    
    public static void main(String[] args) throws Exception {
        try (Connection conn = new Connection()) {
            // используем ресурс
        }  // conn автоматически закрыто и очищено
    }
}

7. Отладка утечек памяти

7.1 Java Flight Recorder (JFR)

# Запуск с JFR
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr MyApp

# Анализ в Java Mission Control
jmc -open recording.jfr

7.2 Heap Dump

# Снять heap dump
jmap -dump:live,format=b,file=heap.bin <PID>

# Анализ с помощью Eclipse MAT
# https://www.eclipse.org/mat/

7.3 Профилирование памяти

public class MemoryProfiler {
    
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        long beforeMemory = runtime.totalMemory() - runtime.freeMemory();
        
        // Создаём объекты
        List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            list.add(new byte[10 * 1024]);  // 10KB каждый
        }
        
        long afterMemory = runtime.totalMemory() - runtime.freeMemory();
        
        System.out.println("Memory used: " + (afterMemory - beforeMemory) / 1024 + " KB");
        
        list = null;
        System.gc();
        
        long afterGc = runtime.totalMemory() - runtime.freeMemory();
        System.out.println("Memory after GC: " + (afterGc - beforeMemory) / 1024 + " KB");
    }
}

8. Ответ на вопрос

Могут ли циклические ссылки создать утечку памяти?

В современной Java (с Mark-and-Sweep GC): НЕТ

  • Циклические ссылки автоматически обнаруживаются и удаляются
  • GC основан на достижимости (reachability), а не на счётчиках ссылок

Реальные утечки памяти в Java:

  • Static references
  • Event listeners (не unsubscribe)
  • ThreadLocal (не clear)
  • Классные loaders
  • Кэши без экспирации
  • Открытые ресурсы (соединения, файлы)

Лучшие практики:

  1. Используйте try-with-resources
  2. Явно удаляйте listeners
  3. Используйте WeakReference для кэшей
  4. Избегайте static коллекций
  5. Тестируйте утечки памяти
  6. Используйте profiler инструменты

Циклические ссылки сами по себе НЕ проблема в Java, но они могут быть признаком плохого дизайна.

Могут ли циклические ссылки создать утечку памяти | PrepBro