Что будешь делать если программа выбрасывает OutOfMemoryException
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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, я буду:
- Собирать информацию: стек вызовов, heap dump, GC логи
- Анализировать: использую MAT или JProfiler для поиска утечек
- Искать причину: memory leak, неправильный алгоритм, отсутствие пагинации
- Проверять: BufferedReaders закрываются? Collections имеют TTL? Нет О(n²)?
- Мониторить: добавляю MemoryMXBean, Prometheus metrics для раннего предупреждения
- Оптимизировать: пагинация, кэширование, lazy loading
- Тестировать: нагрузочный тест с большими объемами данных
Это систематический подход, а не "просто увеличь Xmx"."
Вывод
OOM — это не просто "недостаточно памяти". Это сигнал о том, что:
- Есть memory leak
- Неправильный алгоритм
- Плохое использование ресурсов
- Отсутствует мониторинг
Опытный разработчик не дает OOM появиться на production, потому что мониторит памяти и имеет процесс отладки.