The performance penalty of Class.forName when parsing JPQL and Criteria queries
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
While reviewing this Hibernate Pull Request, I stumbled on the HHH-10746 Jira issue. After doing some research, I realized that this issue was reported multiple times in the past 10 years:
In this post, I’m going to explain why this issue was causing performance issues, and how it got fixed.
Query parsing
Unlike native SQL queries, entity queries (JPQL/HQL and Criteria) are parsed to an Abstract syntax tree (AST) which is later translated to a native SQL query. During syntax tokenization, all expressions containing dots used to be checked so that Hibernate knows if the expression represents a Java constant. This was done by ReflectHelper as follows:
public static Object getConstantValue( String name, ClassLoaderService classLoaderService) { Class clazz; try { clazz = classLoaderService.classForName( StringHelper.qualifier( name ) ); } catch ( Throwable t ) { return null; } try { return clazz.getField( StringHelper.unqualify( name ) ).get( null ); } catch ( Throwable t ) { return null; } }
If only valid Java constant expressions would pass through. However, this method was accessed for every dot expression, therefore for every alias or entity class name.
To illustrate it, consider the following entity query:
List<Post> posts = entityManager.createQuery( "select distinct p " + "from Post p " + "left join fetch p.comments " + "where p.title = :title", Post.class) .setParameter("title", "High-Performance Java Persistence") .getResultList();
When parsing this query, the getConstantValue
method is called with the following arguments:
com.vladmihalcea.book.hpjp.hibernate.fetching.DistinctTest$Post
p.comments
p.title
Therefore, even a trivial entity query can lead to unnecessary calls to Class.forName
.
The cost of Class.forName
Class.forName
is not free. Depending on the underlying application server, it can cause lock contention as illustrated by this Dimo Velev’s article. Pierre-Hugues Charbonneau has a similar article on this topic, which is a wonderful read as well.
This issue has mostly affecting WebLogic, as demonstrated by the aforementioned Hibernate Jira issues, as well as this StackOverflow question.
However, calling Class.forName
with alias fragments or package paths, as it’s the case for our the entity query above, is just a waste of resources. Even if there is no lock contention, the ClassLoader
needs to scan the current classpath, and that’s going to have an impact in a high-performance enterprise application.
The fix
There were several proposals for a fix. Some users have used a ConcurrentHashMap
to store the outcome for each dot expression. However, this solution is more like a band-aid because it doesn’t really address the lock contention issue. Although the ConcurrentHashMap
is optimized for a certain level of concurrency, it still uses locks at a partition level. To be effective, the ConcurrentHashMap
would need to store a large number of entries, therefore putting pressure on memory. For these reasons, I looked for an alternative solution to address this issue.
To overcome this issue, I have added a verification that simply discards any expression that doesn’t follow the Java Naming conventions for a constant.
As long as you express all your Java constants using the fully qualified Class
name and the constant field name contains only uppercase letters and underscores, Hibernate will locate it properly and will discard any other dot expression that should never go through a Class.forName
API call.
public static Object getConstantValue( String name, SessionFactoryImplementor factory) { boolean conventionalJavaConstants = factory .getSessionFactoryOptions() .isConventionalJavaConstants(); Class clazz; try { if ( conventionalJavaConstants && !JAVA_CONSTANT_PATTERN.matcher( name ).find() ) { return null; } ClassLoaderService classLoaderService = factory .getServiceRegistry() .getService( ClassLoaderService.class ); clazz = classLoaderService.classForName( StringHelper.qualifier( name ) ); } catch ( Throwable t ) { return null; } try { return clazz.getField( StringHelper.unqualify( name ) ).get( null ); } catch ( Throwable t ) { return null; } }
When using Hibernate, stick to Java Naming conventions when for constant expressions.
If you’re using non-conventional Java constants, then you’ll have to set the hibernate.query.conventional_java_constants
configuration property to false
. This way, Hibernate will fall back to the previous behavior, treating any expression as a possible candidate for a Java constant.
I'm running an online workshop on the 11th of October about High-Performance SQL.If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
This HHH-4959 is included in Hibernate ORM 5.2.6. If you have any performance-related issue, don’t hesitate to contact me. One of my goals as a Hibernate Developer Advocate is to make sure we tackle all issues that can hurt application performance.
Stay tuned! Hibernate ORM 6.0 comes with a new Query Parser (SQM) which should address this issue at the parser level.
