Введение
Spring JPA с Hibernate – мощный инструмент для работы с базами данных, но при обработке больших объёмов данных можно столкнуться с OutOfMemoryError (OOM). Основная причина – механизм кэширования Hibernate, который хранит загруженные сущности в памяти до завершения транзакции.
Чтобы избежать утечек памяти и зависаний, важно понимать:
- Как работает кэш Hibernate и почему он вызывает OOM. 
- Как - @Transactional(readOnly = true)влияет на кэш и почему он не решает проблему полностью.
- Какие техники загрузки данных позволяют обрабатывать миллионы записей без риска OOM. 
Разберём всё по порядку.
1. Почему кэш Hibernate вызывает OutOfMemoryError?
Hibernate использует двухуровневый кэш:
- Кэш первого уровня (L1 Cache) – автоматически включён и привязан к текущей сессии ( - EntityManager). Все загруженные сущности остаются в памяти до конца транзакции.
- Кэш второго уровня (L2 Cache) – общий для нескольких сессий, требует явной настройки. 
⚠️ Проблема: L1 Cache работает всегда, и если транзакция загружает много данных, то вся эта информация остаётся в памяти, что приводит к OOM.
Пример опасного кода:
@Transactional
public void processAllEntities() {
    List<Entity> entities = repository.findAll(); // Все записи загружаются в память
    for (Entity entity : entities) {
        process(entity);
    }
}❌ Ошибка: пока транзакция не завершится, Hibernate хранит все объекты в памяти → OOM.
Теперь разберём, помогает ли @Transactional(readOnly = true) избежать этой проблемы.
2. Как @Transactional(readOnly = true) влияет на кэш?
Один из популярных способов оптимизации – добавление readOnly = true в @Transactional.
Что даёт readOnly = true?
✔️ Hibernate не отслеживает изменения объектов (отключается Dirty Checking).
✔️ Снижает нагрузку на кэш, так как обновления в базе не выполняются.
✔️ Чуть ускоряет запросы, потому что Hibernate не фиксирует изменения.
⚠️ НО! Кэш первого уровня всё равно остаётся активным, и если запрос загружает миллионы строк, OOM всё равно возможен.
Пример:
@Transactional(readOnly = true)
public List<Entity> getAllEntities() {
    return repository.findAll(); // Все загруженные объекты сохраняются в L1 Cache
}Теперь посмотрим, как эффективно загружать данные без риска OOM.
3. Эффективные стратегии загрузки данных
3.1 Постраничная загрузка (Pagination)
Один из самых простых способов ограничить нагрузку на память – постраничная загрузка (PageRequest).
int pageSize = 1000;
int pageNumber = 0;
Page<Entity> page;
do {
    page = repository.findAll(PageRequest.of(pageNumber, pageSize));
    process(page.getContent());
    entityManager.clear(); // Очистка кэша после каждой страницы
    pageNumber++;
} while (page.hasNext());✔️ Экономит память, загружая фиксированное количество записей за раз.
3.2 Использование Stream с настройкой батча
Если данных слишком много для постраничной загрузки, можно использовать Stream и загружать их батчами (fetchSize).
// Запрос в репозитории
@QueryHints({ @QueryHint(name = "org.hibernate.fetchSize", value = "100") })
@Query("SELECT e FROM Entity e")
Stream<Entity> findAllStream();
// Код сервиса
@PersistenceContext
private EntityManager entityManager;
@Transactional(readOnly = true)
public void processEntities() {
    try (Stream<Entity> stream = repository.findAllStream()) {
        stream.forEach(entity -> {
            process(entity);
            entityManager.detach(entity);  // Очищаем объект из L1 Cache
        });
    }
}✔️ Hibernate загружает данные партиями (fetchSize), а не все сразу.
⚠️ Важно: fetchSize работает не во всех базах данных (например, в MySQL поддержка ограничена).
3.3 Вложенные транзакции
Если требуется обработать большое количество записей в рамках одной бизнес-логики, можно создавать отдельные транзакции для каждой порции данных.
⚠️ Важно: метод с @Transactional(REQUIRES_NEW) должен вызываться через отдельный бин либо через self-inject, иначе транзакция не создастся.
@Service
public class PageProcessorService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processPage(Pageable pageRequest) {
        Page<Entity> page = repository.findAll(pageRequest);
        for (Entity entity : page) {
            process(entity);
        }
    }
}✔️ Hibernate сбрасывает кэш после обработки каждой страницы.
✔️ Отсутствует необходимость чистить кэш "руками" через entityManager.
✔️ Используется Pageable (см. пункт 3.1).
3.4 Автоматическая очистка через Hibernate-слушатели
Чтобы не очищать кэш вручную, можно использовать Hibernate-слушатели:
@Entity
@EntityListeners(ClearCacheListener.class)
public class Entity {
    @Id
    private Long id;
}
@Component
public class ClearCacheListener {
    @PersistenceContext
    private EntityManager entityManager;
    @PostLoad
    public void clearCache(Object entity) {
        entityManager.detach(entity);
    }
}? Hibernate автоматически удаляет объект из L1 Cache после загрузки.
⚠️ Обратите внимание: @PostLoad не всегда срабатывает на коллекциях вложенных сущностей.
Заключение
Чтобы избежать OutOfMemoryError в Spring JPA:
- Используйте постраничную загрузку или стримы с - fetchSize.
- Очищайте L1 Cache ( - clear()/- detach()).
- Используйте вложенные транзакции ( - @Transactional(REQUIRES_NEW)).
- Рассмотрите Hibernate-слушатели или Spring Events для автоматической очистки. 
- Проверяйте настройки JDBC-драйвера, так как некоторые из них могут ограничивать - fetchSize.
Эти техники помогут эффективно работать с большими объёмами данных в Spring JPA!
 
          