Hi @beikov ,
We’ve been investigating issue that’s causing OutOfMemory errors on our server after upgrading from Hibernate 5.6.13 to 6.6.13. I’d appreciate your insights on our findings.
Background
- Previous setup (5.6.13): Used Hibernate native APIs with legacy Criteria
- Current setup (6.6.13): Migrated to JPA Criteria but still use native API initialization due to extensive hbm.xml usage
- Query creation: We obtain CriteriaBuilder from Session and use
Session#createQuery(CriteriaQuery)
Issue Investigation
Hibernate 5.6.13 Behavior
In 5.6.13, we confirmed that Criteria queries don’t use QueryPlanCache. Even when executing identical criteria queries multiple times, getQueryPlanCacheHitCount() and getQueryPlanCacheMissCount() remain at 0, which aligns with our understanding that QueryPlanCache is only for Native/HQL queries.
Hibernate 6.6.13 Behavior
In 6.6.13, we discovered that when using native APIs with JPA Criteria, the behavior changes significantly:
- When creating a TypedQuery via
Session#createQuery(CriteriaQuery), it creates a QuerySqmImpl
- In the constructor, it checks the
hibernate.criteria.copy_tree setting
- Key finding: When
copy_tree=false (default for native APIs), the code explicitly calls setQueryPlanCacheable(true)
- This causes criteria queries to be cached in QueryPlanCache, leading to memory bloat
// Code from QuerySqmImpl Constructor
if ( producer.isCriteriaCopyTreeEnabled() ) {
sqm = criteria.copy( SqmCopyContext.simpleContext() );
}
else {
sqm = criteria;
// Cache immutable query plans by default
setQueryPlanCacheable( true );
}
Root Cause
Since we use native APIs, hibernate.criteria.copy_tree defaults to false. Without copying, each unique CriteriaQuery object becomes a separate cache key, causing the QueryPlanCache to grow indefinitely when creating new criteria instances.
Questions
- Is this intended behavior? Should criteria queries be cached when using native APIs in 6.x?
- Design rationale: Given that QueryPlanCache traditionally doesn’t work for Criteria queries (as confirmed in 5.6.13 and the code comments), why is caching now being enabled for criteria queries in 6.x when using hibernate native APIs? Are we missing some configuration or concept that would make this caching effective?
- Performance impact of copy_tree=true: If we enable
hibernate.criteria.copy_tree=true to prevent the memory issue, what’s the performance impact of deep-copying criteria objects?
- Best practice recommendation: For applications using native APIs with JPA Criteria, what’s the recommended approach to avoid memory issues while maintaining performance?
Test Cases
5.6.13 (Legacy Criteria - No Caching)
SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
Statistics statistics = sessionFactory.getStatistics();
Session session = sessionFactory.openSession();
for (int i = 0; i < 10; i++) {
Criteria criteria = session.createCriteria(SessionInfo.class);
criteria.add(Restrictions.eq("applicationName", "123"));
List list = criteria.list();
System.out.println("Size: " + list.size());
}
System.out.println("Hit count: " + statistics.getQueryPlanCacheHitCount());
System.out.println("Miss count: " + statistics.getQueryPlanCacheMissCount());
Output: Hit count: 0, Miss count: 0
6.6.13 (JPA Criteria with copy_tree=false - Caching Enabled)
Explanation: We explicitly set hibernate.criteria.copy_tree=false in persistence.xml to simulate native API behavior, even though we’re using JPA EntityManagerFactory in this test.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("examplePU");
EntityManager em = emf.createEntityManager();
SessionFactory unwrap = emf.unwrap(SessionFactory.class);
Statistics statistics = unwrap.getStatistics();
HibernateCriteriaBuilder builder = unwrap.getCriteriaBuilder();
Session session = em.unwrap(Session.class);
for (int i = 0; i < 10; i++) {
JpaCriteriaQuery<SessionInfo> query = builder.createQuery(SessionInfo.class);
JpaRoot<SessionInfo> rootElement = query.from(SessionInfo.class);
JpaPredicate equal = builder.equal(rootElement.get("applicationName"), "123" + i);
query.where(equal);
org.hibernate.query.Query<SessionInfo> tp = session.createQuery(query);
List<SessionInfo> resultList = tp.getResultList();
System.out.println("Size: " + resultList.size());
}
System.out.println("Hit count: " + statistics.getQueryPlanCacheHitCount());
System.out.println("Miss count: " + statistics.getQueryPlanCacheMissCount());
Additional Testing
We also tested the same use cases with Native Queries and HQL queries in both Hibernate versions. The QueryPlanCache worked correctly for these query types in both 5.6.13 and 6.6.13, showing proper hit/miss counts and expected caching behavior. This confirms that the issue is specifically related to Criteria queries and the hibernate.criteria.copy_tree setting behavior.
Potential Solutions We’re Considering
- Enable copy_tree: Set
hibernate.criteria.copy_tree=true to prevent caching
- Shifting from Hibernate Native API to JPA Native API in next upgrade.
Any guidance on the intended behavior and recommended approach would be greatly appreciated. Thank you for your time.