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

Можно ли узнать куда утекла память?

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

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

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

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

Определение утечки памяти в Java

Да, можно узнать куда утекла память в Java приложении. Это одна из самых важных компетенций для senior разработчика. Существует целый набор инструментов и техник для диагностики.

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

Сначала нужно понять, что у вас действительно есть утечка:

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;

public class MemoryLeakIndicators {
    
    public static void main(String[] args) throws Exception {
        MemoryMXBean memBean = ManagementFactory.getMemoryMXBean();
        
        // Запустить 10 раз с интервалом
        for (int cycle = 0; cycle < 10; cycle++) {
            long heapUsed = memBean.getHeapMemoryUsage().getUsed();
            long heapMax = memBean.getHeapMemoryUsage().getMax();
            
            System.out.printf("Цикл %d: %d MB / %d MB (%.1f%%)\n",
                cycle,
                heapUsed / (1024 * 1024),
                heapMax / (1024 * 1024),
                (heapUsed * 100.0) / heapMax
            );
            
            // Если значение постоянно растет = утечка
            Thread.sleep(1000);
        }
    }
}

// Результат утечки:
// Цикл 0: 120 MB / 2048 MB (5.8%)
// Цикл 1: 150 MB / 2048 MB (7.3%)
// Цикл 2: 180 MB / 2048 MB (8.7%)
// Цикл 3: 210 MB / 2048 MB (10.2%)
// ... растет постоянно

Шаг 1: Сбор Heap Dump

Способ 1: Через JMap

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

# Сгенерировать heap dump
jmap -dump:live,format=b,file=heap.bin 12345

# Или с гарантией живых объектов
jmap -dump:live,format=b,file=heap.hprof <PID>

Способ 2: Через JCmd (рекомендуется в Java 9+)

jcmd <PID> GC.heap_dump /tmp/heap.hprof

# С паузой приложения
jcmd <PID> GC.heap_dump /tmp/heap.hprof

Способ 3: Автоматический dump при OutOfMemoryError

java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/tmp/heapdump.hprof \
     -Xmx1024m \
     MyApplication

Шаг 2: Анализ Heap Dump в Eclipse MAT

public class MemoryLeakExample {
    
    // Пример утечки 1: Статический список
    private static List<byte[]> staticMemoryLeak = new ArrayList<>();
    
    public void leakyMethod() {
        // Данные добавляются но НИКОГДА не удаляются
        byte[] largeArray = new byte[1024 * 1024 * 10]; // 10 MB
        staticMemoryLeak.add(largeArray);
    }
    
    // Пример утечки 2: Неправильно реализованный singleton
    private static MemoryLeakExample instance;
    
    public static synchronized MemoryLeakExample getInstance() {
        if (instance == null) {
            instance = new MemoryLeakExample();
        }
        return instance; // Живёт вечно
    }
    
    // Пример утечки 3: Listener не удаляется
    public void attachListener(Button button) {
        button.addActionListener(e -> {
            // Анонимный класс держит ссылку на this
            System.out.println(this.toString());
        });
        // Listener удаляется? НЕТ! Жёсткая ссылка остаётся
    }
}

В Eclipse MAT ищете:

  1. Largest Objects: Какие объекты занимают больше всего памяти
  2. Retained Heap: Сколько памяти будет освобождено если удалить объект
  3. GC Roots: Кто держит ссылки на объект

Шаг 3: Использование JProfiler

// JProfiler автоматически отслеживает утечки
public class JProfilerUsage {
    
    public static void main(String[] args) {
        // Запустить с JProfiler:
        // java -agentpath:/path/to/jprofiler/bin/linux-x64/libjprofilerti.so=port=8849 MyApp
        
        // Затем в UI JProfiler:
        // 1. Запустить "Memory" -> "Object Allocations"
        // 2. Найти горячие объекты
        // 3. Посмотреть call stack где создаются
    }
}

Шаг 4: YourKit анализ

# Запуск с YourKit
java -agentpath:/path/to/yourkit/bin/linux-x86-64/libyjpagent.so MyApplication

# Или встроенный snapshot
jcmd <PID> YourKit.snapshot

Programmatic анализ утечек

import com.sun.management.HotSpotDiagnosticMXBean;
import javax.management.MBeanServer;
import java.lang.management.ManagementFactory;

public class HeapDumpGenerator {
    
    public static void generateHeapDump(String filename) {
        try {
            // Получить HotSpot MBean
            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            HotSpotDiagnosticMXBean hotSpotDiagnosticMXBean = 
                ManagementFactory.newPlatformMXBeanProxy(
                    mbs,
                    "com.sun.management:type=HotSpotDiagnostic",
                    HotSpotDiagnosticMXBean.class
                );
            
            // Генерировать dump
            hotSpotDiagnosticMXBean.dumpHeap(filename, true);
            System.out.println("Heap dump saved to: " + filename);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Анализ GC Logs

# Запустить приложение с GC логами
java -Xmx1024m \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -XX:+PrintGCApplicationStoppedTime \
  -Xloggc:gc.log \
  -XX:+UseGCLogFileRotation \
  -XX:NumberOfGCLogFiles=5 \
  -XX:GCLogFileSize=100M \
  MyApplication

# Анализировать с помощью GCViewer
# https://github.com/chewiebug/GCViewer

На что смотреть в GC логе:

  • Время Full GC растет
  • Свободная память после Full GC не растет
  • Heap никогда не очищается полностью

Практический пример отладки утечки

public class ConnectionPoolLeakDebug {
    
    // УТЕЧКА: соединения добавляются но не закрываются
    private static Map<String, Connection> connectionCache = new HashMap<>();
    
    public Connection getConnection(String url) throws SQLException {
        if (!connectionCache.containsKey(url)) {
            Connection conn = DriverManager.getConnection(url);
            connectionCache.put(url, conn); // Жёсткая ссылка
        }
        return connectionCache.get(url);
    }
    
    // Как её найти:
    // 1. Заметить: приложение потребляет все больше памяти
    // 2. jcmd <PID> GC.heap_dump heap.hprof
    // 3. Открыть в Eclipse MAT
    // 4. Найти HashMap с Connection
    // 5. Посмотреть GC roots: обнаружить connectionCache
    // 6. Исправить: добавить closeConnection(), использовать HikariCP
    
    // Правильный способ:
    private static HikariDataSource dataSource = new HikariDataSource();
    
    public Connection getConnectionFixed(String url) throws SQLException {
        return dataSource.getConnection();
    }
}

Использование Monitoring Tools в Production

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.prometheus.PrometheusConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;

public class PrometheusMemoryMonitoring {
    
    public static void setupMemoryMetrics(MeterRegistry registry) {
        // Отслеживать heap usage
        registry.gauge("jvm.memory.used.percent", 
            () -> getHeapUsagePercent());
        
        registry.gauge("jvm.memory.committed", 
            () -> ManagementFactory.getMemoryMXBean()
                .getHeapMemoryUsage().getCommitted() / (1024 * 1024));
    }
    
    private static double getHeapUsagePercent() {
        var heapUsage = ManagementFactory.getMemoryMXBean()
            .getHeapMemoryUsage();
        return (heapUsage.getUsed() * 100.0) / heapUsage.getMax();
    }
}

Инструменты для быстрого анализа

Инструмент 1: Async Profiler (самый быстрый)

./profiler.sh -d 30 -f flame.html <PID>
# Показывает где выделяется память

Инструмент 2: Java Flight Recorder (встроен в JDK 11+)

jcmd <PID> JFR.start duration=60s filename=recording.jfr
jcmd <PID> JFR.dump filename=recording.jfr

# Открыть в JDK Mission Control

Инструмент 3: Сторонние сервисы

  • Datadog APM
  • New Relic
  • Elastic APM

Типичные места утечек

// 1. Статические коллекции
private static List<Data> cache = new ArrayList<>();

// 2. Слушатели событий
component.addListener(listener); // не удален

// 3. Задачи в очереди
private LinkedList<Task> tasks = new LinkedList<>();

// 4. Таймеры
Timer timer = new Timer();
timer.schedule(...); // может не отменяться

// 5. Потоки
new Thread(() -> { }).start(); // без shutdown

// 6. Классы с финализаторами
public void finalize() {} // может привести к утечкам

Быстрая проверка памяти

# Проверить сейчас
jps -l
jmap -heap <PID>
jmap -histo <PID> | head -20

# Сравнить два snapshot'а
jmap -histo:live <PID> > heap1.txt
# Потом после времени
jmap -histo:live <PID> > heap2.txt
diff heap1.txt heap2.txt

Заключение

Да, всегда можно найти утечку памяти в Java приложении. Это требует:

  1. Генерирование heap dumps (jmap, jcmd)
  2. Анализ в Eclipse MAT (Retained Heap, GC Roots)
  3. Чтение GC логов (Full GC тренды)
  4. Профилирование (JProfiler, YourKit, Async Profiler)
  5. Мониторинг в production (Prometheus, Datadog)

Правильный процесс:

  • Заметить проблему (мониторинг памяти)
  • Генерировать heap dump
  • Найти виновника в MAT
  • Исправить код -验证исправление

Это фундаментальная компетенция для любого Java разработчика.

Можно ли узнать куда утекла память? | PrepBro