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

Что будешь делать если программа выбрасывает OutOfMemoryException

1.0 Junior🔥 181 комментариев
#Soft Skills и карьера

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

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

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

OutOfMemoryException: Диагностика и решение

Это один из самых страшных багов на production. OutOfMemoryException означает, что приложение исчерпало доступную память на heap. Расскажу о своей методологии отладки и решения этой проблемы.

Первый шаг: Понять тип OOM

Основные типы OutOfMemoryException:

1. "Java heap space" — heap переполнен
2. "GC overhead limit exceeded" — garbage collector не может освободить память
3. "Direct buffer memory" — переполнена direct memory
4. "Metaspace" или "PermGen" — переполнена метаданные класса
5. "Unable to create new native thread" — не хватает памяти на создание потока

Вторая фаза: Анализ логов и стека вызовов

Полный стек вызовов очень важен:

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3210)
    at com.myapp.UserService.findAllUsers(UserService.java:42) <- ВОТ ПРОБЛЕМА
    at com.myapp.UserController.getAllUsers(UserController.java:15)
    at ...

Моя первая мысль: "Почему в findAllUsers копируется массив неограниченное количество раз?"

Классические причины OOM

1. Memory Leak: объекты не удаляются из памяти

// Плохо: кэш растет бесконечно
public class BadCache {
    private Map<String, List<User>> cache = new HashMap<>();
    
    public void cacheUsers(String key, List<User> users) {
        cache.put(key, users); // никогда не очищается!
    }
}

// Хорошо: используй Caffeine Cache с автоочисткой
public class GoodCache {
    private Cache<String, List<User>> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES) // TTL
        .maximumSize(1000) // максимум элементов
        .build();
}

2. Загрузка всех данных сразу вместо пагинации

// ПЛОХО: если 1 миллион пользователей
List<User> allUsers = userRepository.findAll(); // SELECT * FROM users
// Все 1 млн объектов загружаются в память одновременно

// ХОРОШО: пагинация
Page<User> page = userRepository.findAll(
    PageRequest.of(0, 100) // только 100 элементов за раз
);

// ИЛИ: Stream для больших наборов
try (Stream<User> stream = userRepository.stream()) {
    stream
        .filter(u -> u.isActive())
        .forEach(this::processUser); // обрабатываем по одному
}

3. String конкатенация в цикле

// ПЛОХО: каждая конкатенация создает новую String
String result = "";
for (int i = 0; i < 1_000_000; i++) {
    result += "value-" + i; // O(n²) операций!
}

// ХОРОШО: используй StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1_000_000; i++) {
    sb.append("value-").append(i);
}
String result = sb.toString();

4. Неправильное использование статических коллекций

// ПЛОХО: статический кэш
public class BadStatic {
    private static List<ExpensiveObject> cache = new ArrayList<>(); // никогда не очищается!
    
    public void addToCache(ExpensiveObject obj) {
        cache.add(obj);
    }
}

// ХОРОШО: используй WeakHashMap для автоматической очистки
public class GoodStatic {
    private static Map<Key, ExpensiveObject> cache = new WeakHashMap<>();
}

5. Утечка ресурсов: файлы/соединения не закрываются

// ПЛОХО: BufferedReader не закрывается
public void readFile(String path) throws IOException {
    BufferedReader reader = new BufferedReader(new FileReader(path));
    String line = reader.readLine();
    System.out.println(line);
    // reader никогда не закрывается!
}

// ХОРОШО: try-with-resources
public void readFile(String path) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        String line = reader.readLine();
        System.out.println(line);
    } // reader автоматически закроется
}

Третья фаза: Инструменты для диагностики

1. JVM аргументы для сбора информации

# Запусти приложение с дополнительными параметрами
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/tmp/heapdump.hprof \
     -Xmx2g \
     -Xms2g \
     -XX:+PrintGCDetails \
     -XX:+PrintGCDateStamps \
     -Xloggc:/tmp/gc.log \
     -jar myapp.jar

Что это делает:

  • -XX:+HeapDumpOnOutOfMemoryError — создает dump heap при OOM
  • -XX:HeapDumpPath=... — куда сохранить dump
  • -Xmx2g — максимум 2GB heap
  • -XX:+PrintGCDetails — логировать сборку мусора

2. Анализ Heap Dump

# Используй Eclipse MAT (Memory Analyzer Tool)
# или JProfiler

# Командная строка:
jhat /tmp/heapdump.hprof
# Откроется веб-интерфейс на http://localhost:7000

В MAT ищешь:

  • "Leak Suspects" — возможные утечки
  • "Biggest Objects" — что занимает больше всего памяти
  • "GC Roots" — откуда ссылаются объекты

3. Профилирование JVM

# С помощью visualvm
jvisualvm

# Или встроенный jconsole
jconsole

# Или коммерческие: YourKit, JProfiler

Четвертая фаза: GC анализ

Понимание Garbage Collector

# Анализ GC логов
java -XX:+PrintGCDetails gc.log | less

# Ищешь такие признаки:
# 1. Full GC каждую минуту (признак утечки)
# 2. Heap не очищается (объекты держат ссылки)
# 3. GC time > 50% (приложение тратит время на очистку)

Выбор правильного GC алгоритма

# G1GC (для heap > 4GB, с Java 9+ по умолчанию)
java -XX:+UseG1GC 
     -XX:G1HeapRegionSize=16M 
     -XX:G1NewCollectionHaloSize=8 \
     -jar app.jar

# ZGC (для очень низких пауз, Java 11+)
java -XX:+UnlockExperimentalVMOptions \
     -XX:+UseZGC \
     -jar app.jar

# Shenandoah (альтернатива ZGC)
java -XX:+UnlockExperimentalVMOptions \
     -XX:+UseShenandoahGC \
     -jar app.jar

Пятая фаза: Практическое решение

Код для мониторинга памяти

@Component
public class MemoryMonitor {
    @Scheduled(fixedRate = 60000) // каждую минуту
    public void logMemoryStatus() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        
        long used = heapUsage.getUsed() / 1024 / 1024; // MB
        long max = heapUsage.getMax() / 1024 / 1024; // MB
        long committed = heapUsage.getCommitted() / 1024 / 1024; // MB
        
        double percentUsed = (double) used / max * 100;
        
        logger.info("Heap Usage: {} MB / {} MB ({:.1f}%)", used, max, percentUsed);
        
        // Тревога если > 90%
        if (percentUsed > 90) {
            logger.error("КРИТИЧНО: Heap заполнен на {:.1f}%", percentUsed);
            // Отправить алерт
        }
    }
}

Кэширование с лимитом

@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new CaffeineCacheManager();
    }
}

@Component
public class UserCache {
    private final Cache<Long, User> cache = Caffeine.newBuilder()
        .maximumSize(10_000) // максимум 10k пользователей в кэше
        .expireAfterWrite(10, TimeUnit.MINUTES) // удалить через 10 минут
        .recordStats() // собирать статистику
        .build();
}

Пагинированная загрузка больших наборов данных

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public void processAllUsers() {
        int pageSize = 1000;
        int page = 0;
        Page<User> pageData;
        
        do {
            pageData = userRepository.findAll(
                PageRequest.of(page, pageSize)
            );
            
            for (User user : pageData) {
                // обрабатываем пользователя
                processUser(user);
            }
            
            page++;
        } while (pageData.hasNext());
    }
}

Чеклист при OOM

☐ Собрать heap dump (-XX:+HeapDumpOnOutOfMemoryError)
☐ Проверить memory leaks (MAT, JProfiler)
☐ Проверить GC логи
☐ Увеличить heap (-Xmx)
☐ Проверить алгоритм: есть ли O(n²)?
☐ Проверить циклы: может ли коллекция расти бесконечно?
☐ Проверить кэши: есть ли TTL?
☐ Проверить resources: закрываются ли файлы/соединения?
☐ Проверить пагинацию: загружаются ли все данные сразу?
☐ Запустить профилировщик
☐ Проверить зависимости: может быть library утекает память?

На собеседовании ответ

"Если программа выбросит OutOfMemoryException, я буду:

  1. Собирать информацию: стек вызовов, heap dump, GC логи
  2. Анализировать: использую MAT или JProfiler для поиска утечек
  3. Искать причину: memory leak, неправильный алгоритм, отсутствие пагинации
  4. Проверять: BufferedReaders закрываются? Collections имеют TTL? Нет О(n²)?
  5. Мониторить: добавляю MemoryMXBean, Prometheus metrics для раннего предупреждения
  6. Оптимизировать: пагинация, кэширование, lazy loading
  7. Тестировать: нагрузочный тест с большими объемами данных

Это систематический подход, а не "просто увеличь Xmx"."

Вывод

OOM — это не просто "недостаточно памяти". Это сигнал о том, что:

  • Есть memory leak
  • Неправильный алгоритм
  • Плохое использование ресурсов
  • Отсутствует мониторинг

Опытный разработчик не дает OOM появиться на production, потому что мониторит памяти и имеет процесс отладки.

Что будешь делать если программа выбрасывает OutOfMemoryException | PrepBro