The performance penalty of Class.forName when parsing JPQL and Criteria queries

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 class path, 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.

If you enjoyed this article, I bet you are going to love my book 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.

Enter your email address to follow this blog and receive notifications of new posts by email.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s