← Назад к вопросам
Могут ли циклические ссылки создать утечку памяти
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
- Кэши без экспирации
- Открытые ресурсы (соединения, файлы)
Лучшие практики:
- Используйте try-with-resources
- Явно удаляйте listeners
- Используйте WeakReference для кэшей
- Избегайте static коллекций
- Тестируйте утечки памяти
- Используйте profiler инструменты
Циклические ссылки сами по себе НЕ проблема в Java, но они могут быть признаком плохого дизайна.