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

Как найти утечку памяти в Java

2.0 Middle🔥 191 комментариев
#JVM и управление памятью#Тестирование

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

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

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

Как найти утечку памяти в Java

Утечка памяти в Java — это когда объекты больше не используются, но на них остаются ссылки, препятствующие их удалению сборщиком мусора. Это может привести к OutOfMemoryError. Расскажу о методах диагностики.

Шаг 1: Определить, что есть утечка

Применить мониторинг памяти:

Runtime runtime = Runtime.getRuntime();

// Общая память, выделенная для JVM
long totalMemory = runtime.totalMemory(); // в байтах

// Свободная память
long freeMemory = runtime.freeMemory();

// Используемая память
long usedMemory = totalMemory - freeMemory;

System.out.printf("Total: %d MB, Free: %d MB, Used: %d MB%n",
    totalMemory / 1024 / 1024,
    freeMemory / 1024 / 1024,
    usedMemory / 1024 / 1024);

Повторяйте это периодически. Если используемая память растёт и никогда не падает — есть утечка.

Шаг 2: Инструмент JVisualVM

В JDK входит jvisualvm — графический инструмент мониторинга:

jvisualvm

Он показывает:

  • График использования памяти
  • Heap dump
  • GC активность
  • Потоки
  • CPU использование

Как использовать:

  1. Запустите jvisualvm
  2. Выберите ваше приложение из списка
  3. Перейдите на вкладку Monitor
  4. Следите за графиком памяти

Шаг 3: Собрать Heap Dump

Способ 1: Через jmap (командная строка)

# Найти PID приложения
jps
# 12345 MyApp
# 54321 SomeOtherApp

# Создать heap dump
jmap -dump:live,format=b,file=heap.bin 12345

Способ 2: Через jvisualvm

  1. Правый клик на процесс
  2. "Heap Dump"
  3. Файл сохранится автоматически

Способ 3: В коде (программно)

import com.sun.management.HotSpotDiagnosticsMXBean;
import java.lang.management.ManagementFactory;

public void dumpHeap(String filename) {
    try {
        HotSpotDiagnosticsMXBean bean = ManagementFactory.getPlatformMXBean(
            HotSpotDiagnosticsMXBean.class
        );
        bean.dumpHeap(filename, true);
        System.out.println("Heap dumped to " + filename);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Шаг 4: Анализировать Heap Dump

Способ 1: jhat (Java Heap Analysis Tool)

jhat heap.bin
# Откроет веб-интерфейс на localhost:7000

Способ 2: Eclipse Memory Analyzer (MAT)

Это мощный инструмент анализа:

# Скачать с eclipse.org
# Открыть heap.bin в MAT

Mat покажет:

  • Какие объекты занимают больше всего памяти
  • Какие ссылки удерживают объекты
  • "Suspect" объекты, вероятно являющиеся причиной утечки

Шаг 5: Анализировать в коде

Пример утечки памяти:

public class MemoryLeakExample {
    // Статический список, никогда не очищается
    private static List<byte[]> memoryCache = new ArrayList<>();
    
    public void addToCache(byte[] data) {
        memoryCache.add(data); // Объекты остаются в памяти навсегда!
    }
}

Как найти в heap dump:

  1. Ищите List/HashMap/Queue, содержащие много объектов
  2. Проверьте статические поля
  3. Ищите циклические ссылки
  4. Ищите слушатели (listeners), которые не отписались

Шаг 6: Типичные причины утечек

1. Статические коллекции

// ПЛОХО: объекты в памяти навсегда
private static List<MyObject> cache = new ArrayList<>();

// ХОРОШО: с ограничением размера
private static List<MyObject> cache = new LinkedList<MyObject>() {
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > 1000; // Максимум 1000 элементов
    }
};

// ИЛИ: используй WeakReference
private static Map<String, WeakReference<MyObject>> cache = new HashMap<>();

2. Неотписанные слушатели

// ПЛОХО
public void subscribe() {
    button.addClickListener(this::handleClick);
    // Никогда не отписываемся!
}

// ХОРОШО
public void subscribe() {
    button.addClickListener(this::handleClick);
}

public void cleanup() {
    button.removeClickListener(this::handleClick);
}

3. Незакрытые ресурсы

// ПЛОХО
public void readFile() throws IOException {
    FileInputStream fis = new FileInputStream("data.txt");
    // ... читаем ...
    // Забыли закрыть!
}

// ХОРОШО: try-with-resources
public void readFile() throws IOException {
    try (FileInputStream fis = new FileInputStream("data.txt")) {
        // ... читаем ...
        // Автоматически закроется
    }
}

4. Циклические ссылки

public class Parent {
    private Child child = new Child(this); // child ссылается на parent
}

public class Child {
    private Parent parent; // parent ссылается на child
    
    public Child(Parent parent) {
        this.parent = parent;
    }
}

// Удаление parent не удаляет child, и наоборот (в некоторых сценариях)

5. ThreadLocal утечки

// ПЛОХО: если не очистить
public class ThreadLocalLeak {
    private static ThreadLocal<Connection> connectionHolder = 
        ThreadLocal.withInitial(() -> new Connection());
    
    // Если поток переиспользуется (thread pool), 
    // Connection останется в памяти
}

// ХОРОШО: всегда очищайте
public void cleanup() {
    connectionHolder.remove();
}

Шаг 7: Инструменты мониторинга

JMX (Java Management Extensions)

import java.lang.management.*;

public class MemoryMonitor {
    public static void monitorMemory() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        
        System.out.println("Heap:");
        System.out.println("Init: " + heapUsage.getInit() / 1024 / 1024 + " MB");
        System.out.println("Used: " + heapUsage.getUsed() / 1024 / 1024 + " MB");
        System.out.println("Max: " + heapUsage.getMax() / 1024 / 1024 + " MB");
        System.out.println("Committed: " + heapUsage.getCommitted() / 1024 / 1024 + " MB");
    }
}

Micrometer + Prometheus

// Для production мониторинга
@Configuration
public class MetricsConfig {
    @Bean
    public MeterRegistry meterRegistry() {
        PrometheusMeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
        
        // Память
        Gauge.builder("jvm.memory.used", 
            () -> Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())
            .baseUnit("bytes")
            .register(registry);
        
        return registry;
    }
}

Пошаговый процесс диагностики

1. Заметить проблему
   ↓
2. Мониторить память (Runtime API или jvisualvm)
   ↓
3. Подтвердить утечку (график памяти растёт)
   ↓
4. Создать heap dump (jmap или jvisualvm)
   ↓
5. Анализировать dump (MAT или jhat)
   ↓
6. Найти объект, занимающий память
   ↓
7. Найти ссылку на объект (Path to Root)
   ↓
8. Найти код, создающий эту ссылку
   ↓
9. Исправить код (удалить ссылку, очистить коллекцию и т.д.)
   ↓
10. Повторить тестирование

Флаги JVM для диагностики

java -Xmx512m \
     -XX:+PrintGCDetails \
     -XX:+PrintGCTimeStamps \
     -Xloggc:gc.log \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=./heapdump.hprof \
     MyApplication

Где:

  • -Xmx512m — максимум памяти 512 MB
  • -XX:+PrintGCDetails — выводить детали сборки мусора
  • -XX:+HeapDumpOnOutOfMemoryError — создать dump при ошибке

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

  1. Используй try-with-resources для всех ресурсов
  2. Не кешируй без необходимости, или используй WeakReference
  3. Отписывайся от слушателей в cleanup методах
  4. Избегай статических коллекций, если не нужна глобальная кэш
  5. Мониторь память в production через JMX или Micrometer
  6. Регулярно проверяй GC логи на странные паттерны