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

Как определить из-за кэша ли происходит периодическая перезагрузка сервиса из-за OutOfMemoryError?

2.4 Senior🔥 141 комментариев
#JVM и управление памятью

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

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

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

Диагностика OutOfMemoryError, вызванной кэшем

OutOfMemoryError из-за кэша — это частая проблема в production. Невозможно корректно очистить кэш или кэш растёт неконтролируемо, что приводит к исключению и перезагрузке сервиса.

1. Анализ Java GC логов

Первый шаг — посмотреть на GC логи при запуске приложения с флагами.

java -Xmx1024m \
  -Xms512m \
  -XX:+UseG1GC \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -XX:+PrintGCTimeStamps \
  -Xloggc:/var/log/gc.log \
  -XX:+UseGCLogFileRotation \
  -XX:NumberOfGCLogFiles=10 \
  -XX:GCLogFileSize=10M \
  -jar application.jar

В логе ищи:

  • Постоянный рост heap'а между Full GC сборками
  • Всё более частые Full GC сборки
  • Память не освобождается после Full GC (признак утечки)
2024-03-20T10:15:30.123+0000: 45.567: [Full GC (Allocation Failure) -- Heap before GC invocations=18:
 par new generation   total 680000K, used 679999K
 eden space 598848K, 100% used
 from space 81152K, 100% used
 to   space 81152K,   0% used
tenured generation   total 1862528K, used 1862527K
...
Heap after GC invocations=18:
 par new generation   total 680000K, used 98304K
 eden space 598848K,   0% used
 from space 81152K,  98% used
 to   space 81152K,   0% used
tenured generation   total 1862528K, used 1862527K --> ПРОБЛЕМА: стареющее поколение не освобождается

2. Heap Dump анализ

Вызови heap dump во время проблемы или перед перезагрузкой.

# Получить dump (PID процесса Java)
jmap -dump:live,format=b,file=heap.bin <PID>

# Или через JMX если включен
jcmd <PID> GC.heap_dump /var/dumps/heap.bin

Анализ в Eclipse MAT или JProfiler:

// Найди "Top consumers" - объекты, занимающие много памяти
// Обычно это:
// - Map entries (если используется неограниченный кэш)
// - String objects (если кэшируются строки)
// - Arrays (если кэшируются большие коллекции)

Основные шаги анализа:

  1. Откройте heap dump в MAT
  2. Перейдите в "Dominator Tree"
  3. Найдите классы, занимающие больше всего памяти
  4. Проверьте ссылки (incoming references)
  5. Найдите кэш-класс в цепочке ссылок

3. Практический пример: диагностика утечки кэша

public class CacheLeakExample {
    // ПРОБЛЕМА: не ограниченный Map кэш
    private static final Map<String, LargeObject> cache = new HashMap<>();
    
    public static void main(String[] args) throws Exception {
        // Симуляция: кэш растёт без ограничений
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String key = "key_" + i;
            cache.put(key, new LargeObject());
            if (i % 10000 == 0) {
                System.out.println("Cache size: " + cache.size());
            }
        }
    }
    
    static class LargeObject {
        byte[] data = new byte[1024 * 1024]; // 1 MB
    }
}

Диагностика:

  • Heap dump покажет множество LargeObject в HashMap
  • Incoming references: HashMap → CacheLeakExample (static field)
  • Вывод: static Map без механизма очистки = утечка памяти

4. Типичные причины утечек в кэшах

1. Неограниченный размер кэша

// ПЛОХО
private static final Map<String, Object> cache = new HashMap<>();

public static void cacheValue(String key, Object value) {
    cache.put(key, value); // Растёт бесконечно!
}

2. Отсутствие TTL (Time-To-Live)

// ПЛОХО: старые значения никогда не удаляются
private static final Map<String, CacheEntry> cache = new HashMap<>();

class CacheEntry {
    Object value;
    long createdAt;
}

3. Классическая утечка в Collection

// ПЛОХО: listener'ы не удаляются
private List<ChangeListener> listeners = new ArrayList<>();

public void addListener(ChangeListener listener) {
    listeners.add(listener); // Никогда не удаляется!
}

4. Strong reference в кэше к объектам

// ПЛОХО: даже если объект больше не используется в коде,
// он живёт в кэше
public class DataCache {
    private Map<Long, DataObject> cache = new HashMap<>();
    
    public void cache(DataObject obj) {
        cache.put(obj.getId(), obj);
    }
}

5. Правильный кэш с ограничением

public class LimitedCache<K, V> {
    private final LinkedHashMap<K, V> cache;
    private final int maxSize;
    
    public LimitedCache(int maxSize) {
        this.maxSize = maxSize;
        // LinkedHashMap с LRU эвикцией
        this.cache = new LinkedHashMap<K, V>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > maxSize;
            }
        };
    }
    
    public synchronized void put(K key, V value) {
        cache.put(key, value);
    }
    
    public synchronized V get(K key) {
        return cache.get(key);
    }
}

// Использование
LimitedCache<String, String> cache = new LimitedCache<>(10000);
cache.put("key", "value");

6. Использование правильных кэш-библиотек

// Google Guava Cache - с TTL и максимальным размером
Cache<String, String> cache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build();

cache.put("key", "value");
String value = cache.getIfPresent("key");

// Caffeine - современная альтернатива Guava
com.github.benmanes.caffeine.cache.Cache<String, String> caffeineCache = 
    Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .recordStats()
        .build();

// Spring Cache с аннотациями
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
    return userService.findById(id);
}

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

JVM метрики для мониторинга:

MemoryMXBean memoryMxBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryMxBean.getHeapMemoryUsage();

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");

Prometheus метрики:

// Метрика размера кэша
Gauge cacheSize = Gauge.build()
    .name("cache_size")
    .help("Current cache size")
    .labelNames("cache_name")
    .register();

cacheSize.labels("user_cache").set(cache.size());

8. Чеклист диагностики

  1. Включи GC логи с флагами PrintGCDetails
  2. Анализируй паттерн GC: растёт ли heap после каждого Full GC?
  3. Создай heap dump перед перезагрузкой
  4. Открой в MAT/JProfiler и найди Top Consumers
  5. Проверь incoming references для подозрительных объектов
  6. Найди статические кэши в коде
  7. Проверь, есть ли TTL и max-size ограничения
  8. Используй правильные библиотеки: Guava Cache, Caffeine, Spring Cache
Как определить из-за кэша ли происходит периодическая перезагрузка сервиса из-за OutOfMemoryError? | PrepBro