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

Что произойдет если два объекта ссылаются друг на друга?

2.0 Middle🔥 141 комментариев
#Другое

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

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

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

Циклические ссылки между объектами в Java

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

1. Обычные ссылки и сборка мусора

Циклические ссылки НЕ вызывают утечки памяти

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

// Создание циклической ссылки
public static void main(String[] args) {
    Person alice = new Person("Alice");
    Person bob = new Person("Bob");
    
    // Циклическая ссылка
    alice.friend = bob;    // Alice ссылается на Bob
    bob.friend = alice;    // Bob ссылается на Alice
    
    // Обе переменные выходят из области видимости
    // alice = null;  // явно обнуляем
    // bob = null;    // явно обнуляем
}

// Что происходит:
// 1. alice = null  -> объект Alice становится недостижимым (в основном)
// 2. bob = null   -> объект Bob становится недостижимым (в основном)
// 3. Несмотря на циклические ссылки между ними,
//    оба объекта БУДУТ собраны сборщиком мусора,
//    потому что на них больше нет ссылок из "живых" объектов

2. Почему циклические ссылки не вызывают утечек

Java использует Garbage Collector с алгоритмом mark-and-sweep:

// Алгоритм сборки мусора (упрощённо):
// 1. Mark phase (пометка):
//    - Начинаем с root references (переменные в стеке, статические поля)
//    - Помечаем все достижимые объекты
//    - Циклические ссылки НЕ мешают, потому что объекты всё равно помечены

// 2. Sweep phase (удаление):
//    - Удаляем все непомеченные объекты

// Пример:
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friend = bob;
bob.friend = alice;

// Граф объектов:
// Heap (живой):
//   alice ---------> [Person(Alice)] <--------
//   bob   ---------> [Person(Bob)]   <--------
//                         |                 |
//                         +<--------+-------+
//                         (циклическая ссылка)

// Когда переменные выходят из области видимости:
// alice = null;  bob = null;

// Граф объектов:
// Heap (мусор):
//   [Person(Alice)] <--------+
//        |                    |
//        +<--------+----------+
//       [Person(Bob)]
//   
// Оба объекта не достижимы из root references
// Будут помечены как мусор и удалены

3. Практический пример

public class CircularReferenceExample {
    
    static class Node {
        int value;
        Node next;
        
        Node(int value) {
            this.value = value;
        }
    }
    
    public static void demonstrateCircularReferences() {
        // Создание циклического списка
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        
        node1.next = node2;
        node2.next = node3;
        node3.next = node1;  // ЦИКЛ!
        
        // Переход по циклу
        traverseCircle(node1, 10);  // выведет 1,2,3,1,2,3,1,2,3,1
        
        // После выхода из метода все три узла могут быть собраны
        // потому что на них нет ссылок из живых объектов
    }
    
    static void traverseCircle(Node start, int limit) {
        Node current = start;
        for (int i = 0; i < limit; i++) {
            System.out.print(current.value + ",");
            current = current.next;
        }
    }
}

4. Проблемы при сериализации

import java.io.*;

public class SerializationIssues {
    
    static class LinkedObject implements Serializable {
        String name;
        LinkedObject next;
        
        LinkedObject(String name) {
            this.name = name;
        }
    }
    
    public static void serializeWithCircularReference() throws Exception {
        LinkedObject obj1 = new LinkedObject("First");
        LinkedObject obj2 = new LinkedObject("Second");
        
        obj1.next = obj2;
        obj2.next = obj1;  // Циклическая ссылка
        
        // При сериализации может возникнуть StackOverflowError
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        
        try {
            oos.writeObject(obj1);  // ОПАСНО!
            // oos запишет obj1 -> obj2 -> obj1 -> obj2 -> ...
            // Бесконечная рекурсия
        } catch (StackOverflowError e) {
            System.out.println("Stack overflow during serialization!");
        }
    }
}

5. Циклические ссылки в JSON-сериализаторах

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.fasterxml.jackson.annotation.JsonBackReference;

public class JsonSerializationWithCycles {
    
    // Правильный способ - использовать аннотации Jackson
    static class Parent {
        String name;
        
        @JsonManagedReference
        Child child;
        
        Parent(String name) {
            this.name = name;
        }
    }
    
    static class Child {
        String name;
        
        @JsonBackReference
        Parent parent;
        
        Child(String name) {
            this.name = name;
        }
    }
    
    public static void main(String[] args) throws Exception {
        Parent parent = new Parent("Parent");
        Child child = new Child("Child");
        
        parent.child = child;
        child.parent = parent;  // Циклическая ссылка
        
        ObjectMapper mapper = new ObjectMapper();
        
        // Jackson корректно обработает циклические ссылки
        String json = mapper.writeValueAsString(parent);
        System.out.println(json);
        // Output: {"name":"Parent","child":{"name":"Child"}}
        // parent не сериализуется в child, чтобы избежать цикла
    }
}

6. Проблемы при использовании Streams

public class StreamWithCycles {
    
    static class Graph {
        String id;
        List<Graph> neighbors;
        
        Graph(String id) {
            this.id = id;
            this.neighbors = new ArrayList<>();
        }
        
        // НЕПРАВИЛЬНО - может зависнуть на циклах
        void printGraphBad() {
            neighbors.stream()
                .forEach(node -> {
                    System.out.println(node.id);
                    node.printGraphBad();  // Бесконечная рекурсия на цикле!
                });
        }
        
        // ПРАВИЛЬНО - используем посещённые узлы
        void printGraphGood(Set<String> visited) {
            if (visited.contains(id)) return;  // Уже посетили
            
            visited.add(id);
            System.out.println(id);
            
            neighbors.stream()
                .forEach(node -> node.printGraphGood(visited));
        }
    }
    
    public static void main(String[] args) {
        Graph a = new Graph("A");
        Graph b = new Graph("B");
        Graph c = new Graph("C");
        
        a.neighbors.add(b);
        b.neighbors.add(c);
        c.neighbors.add(a);  // Цикл: A -> B -> C -> A
        
        // a.printGraphBad();  // ОПАСНО!
        a.printGraphGood(new HashSet<>());  // БЕЗОПАСНО
    }
}

7. Weak References для избежания циклических ссылок

import java.lang.ref.WeakReference;

public class WeakReferencesExample {
    
    static class Node {
        String value;
        WeakReference<Node> parent;  // Слабая ссылка
        
        Node(String value) {
            this.value = value;
        }
    }
    
    public static void main(String[] args) {
        Node parent = new Node("Parent");
        Node child = new Node("Child");
        
        // Используем WeakReference для parent ссылки в child
        child.parent = new WeakReference<>(parent);
        
        // Даже если есть циклическая ссылка через WeakReference,
        // объект parent может быть собран сборщиком мусора,
        // если на него нет других сильных ссылок
        
        parent = null;  // Удаляем сильную ссылку
        System.gc();
        
        // parent объект может быть собран
        Node retrievedParent = child.parent.get();
        System.out.println(retrievedParent == null);  // Может быть true
    }
}

8. Обнаружение циклических ссылок

public class CycleDetection {
    
    static class Node {
        int value;
        Node next;
        
        Node(int value) {
            this.value = value;
        }
    }
    
    // Алгоритм Floyd (Tortoise and Hare)
    static boolean hasCycle(Node head) {
        if (head == null || head.next == null) return false;
        
        Node slow = head;    // Черепаха - один шаг
        Node fast = head;    // Заяц - два шага
        
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            
            if (slow == fast) {
                return true;  // Обнаружена циклическая ссылка
            }
        }
        
        return false;
    }
    
    public static void main(String[] args) {
        // Создание циклического списка
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        
        node1.next = node2;
        node2.next = node3;
        node3.next = node1;  // Цикл
        
        System.out.println("Has cycle: " + hasCycle(node1));  // true
    }
}

Summary таблица

СценарийРезультатПримечание
Обычные объектыGC удалитJava GC справляется с циклами
СериализацияStackOverflowErrorНужно использовать аннотации
Stream операцииБесконечностьНужно отслеживать посещённые узлы
Weak ReferencesМогут быть собраныИспользуются для разрыва циклов
ОбнаружениеFloyd алгоритмO(n) память, O(n) время

Best Practices

  1. Не беспокойся о циклических ссылках в памяти — GC их правильно обработает
  2. Используй аннотации при JSON-сериализации — @JsonBackReference/@JsonManagedReference
  3. Отслеживай посещённые узлы при обходе графов
  4. Используй WeakReference если нужно избежать циклических ссылок
  5. Обнаруживай циклы в структурах данных перед их использованием

Итог: Циклические ссылки НЕ вызывают утечки памяти в Java благодаря продвинутому Garbage Collector. Однако они могут создавать проблемы при сериализации, обходе графов и других операциях, требующих рекурсии. Правильное использование аннотаций, отслеживание посещённых узлов и слабые ссылки помогают справиться с циклическими ссылками.