← Назад к вопросам
Можно ли узнать куда утекла память?
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 ищете:
- Largest Objects: Какие объекты занимают больше всего памяти
- Retained Heap: Сколько памяти будет освобождено если удалить объект
- 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 приложении. Это требует:
- Генерирование heap dumps (jmap, jcmd)
- Анализ в Eclipse MAT (Retained Heap, GC Roots)
- Чтение GC логов (Full GC тренды)
- Профилирование (JProfiler, YourKit, Async Profiler)
- Мониторинг в production (Prometheus, Datadog)
Правильный процесс:
- Заметить проблему (мониторинг памяти)
- Генерировать heap dump
- Найти виновника в MAT
- Исправить код -验证исправление
Это фундаментальная компетенция для любого Java разработчика.