Every time we benchmark and profile one of our Hibernate projects for whatever reason we can always count on our good old friend — reflection — to show up and say hello.

Hibernate projects work with user-defined Java objects. Hence, all these projects need a way to read and update fields and create new instances of such objects. Java Reflection can do this, but is it always the best approach? As most abstractions there are downsides to it. It can introduce some unnecessary performance overhead and extra checks/behaviour that may not be required for our use cases. It can also make performance flamegraphs noisier (that is where we have seen it!). Each project is still left to deal with JPMS access rules and member access conventions on its own. On top of that, reflection requires extra configuration for native builds. On the other hand, reflection is a standard JDK API, is simple enough on the surface, and has stood the test of time. Can we do any better than that?

This is certainly not a new problem, and it is not the first time we asked these questions of ourselves. Over the years, each project was experimenting within their own codebases in isolation to find a suitable approach. Solutions we came up with were either partial, not easily shareable with no simple way of plugging an alternative implementation, and some just not suitable because they were tailored to work with certain access patterns. Yes, this matters because the best-performing approach depends on the task — for example, traversing a validation graph versus loading database results into entities.

In an attempt to solve this, we started Hibernate Accessor — a unified state access engine designed to eliminate unnecessary reflection overhead and provide solution for specific access patterns across the whole Hibernate project portfolio.

In this series we will take you through the journey starting from giving you an idea why existing JDK APIs do not solve the problem for us, then walk you through hidden surprises and tricks of bytecode generation and JPMS as well as some of its shortcomings. We will show you how and what we have benchmarked to select a good default all-rounder solution, and finishing the series by exploring what shifting code generation to the build time can achieve and how do we plan to leverage that.

Was not this solved already?!

JDK is constantly improving and one could rightfully ask — did not method handles already solve the slow reflection access problems?

Reflection (java.lang.reflect): an old flame in our graphs

Plain vanilla reflection, available since forever, something most of the frameworks and libraries relied upon at some point in time or still do. It is easy to work with:

Field name = Person.class.getDeclaredField("name");
name.setAccessible(true); (1)
Object value = name.get(person); (2)
  1. Need to make the reflection member accessible, to have any access to non-public members. This also means that in JPMS, one needs to add corresponding opens directives to allow for deep reflection

  2. Reads the field reflectively. Java access checks are suppressed by setAccessible(true), but this does not guarantee that the JIT can optimize the access as effectively as a direct field read.

Historically, reflection had a reputation for being slow. Though in JDK 18, reimplemented core reflection using method handles under JEP 416. This lets reflection benefit from method-handle optimizations, but also means it shares some of their performance constraints. To understand why this does not fully solve our performance side of the problem, let’s look at method handles directly.

Method handles: getting a handle on things, or…​

Method handles check access permissions when they are created, rather than on every invocation while Reflection normally checks access on each use, though as discussed above, these checks can be suppressed with setAccessible(true). With access checks out of the invocation path, the difference comes down to the invocation mechanics and whether JIT can make optimisations.

You may have seen some synthetic benchmarks showing that method handles can give you near direct member access. But there is a catch — for method handle to reach its peak performance, JIT needs to be able to treat a reference to such handle as a constant:

// The textbook way to use method handles:
private static final MethodHandle MH = MethodHandles.lookup() (1)
    .findVirtual(Person.class, "getName", MethodType.methodType(String.class));

// The actual library reality:
public class DynamicPropertyReader {
    private final MethodHandle mh; (2)

    public DynamicPropertyReader(MethodHandle mh) {
        this.mh = mh;
    }

    public Object read(Object target) throws Throwable {
        return this.mh.invoke(target); (3)
    }
}
  1. The static final reference is what makes things fast — the JIT can constant-fold it and inline the underlying call.

  2. The library reality — we discover the user types at runtime and can only create dynamic instance fields.

  3. Is not really inlinable by JIT.

As explained in Jorn Vernee’s MethodHandle primer, MethodHandle invocation relies on the handle being an exact compile-time constant for the JIT to inline the underlying LambdaForm.

LambdaMetafactory: no fields attached

LambdaMetafactory is a lesser known but very powerful instrument to get as close to direct getter calls as possible. It generates lightweight functional interface implementations at runtime that are JIT friendly and inline nicely.

CallSite site = LambdaMetafactory.metafactory(
    lookup, (1)
    "apply",
    MethodType.methodType(Function.class),
    MethodType.methodType(Object.class, Object.class),
    lookup.findVirtual(Person.class, "getName", MethodType.methodType(String.class)),
    MethodType.methodType(String.class, Person.class)
);
Function<Person, String> getter = (Function<Person, String>) site.getTarget().invokeExact(); (2)
  1. Has to be a lookup with enough access to get you to the required members.

  2. Get the functional interface implementation generated at runtime.

This approach is explored in detail by Geoffrey De Smet in Java Reflection, but much faster, and under specific conditions it reaches that peak performance, but it comes short when one needs to work with fields directly instead of getters/setters. That is because the factory ultimately works with methods and cannot emit a GETFIELD instruction directly.

One by one, or all for one?

The best-performing approach also depends on how exactly we access the state. So far, we have looked at accessing a single member at a time — reading a field, invoking a getter, or calling a constructor. This usually is a good fit for "random access" cases like traversing a validation graph, where we may read one property, inspect its value and either traverse into it or move to the next sibling.

But there are also cases where we need to access several properties of the same object in one operation, without coming back to read them again later. For example, when loading a database row into an entity, Hibernate ORM needs to write the values of multiple mapped properties. Similarly, when extracting an entity’s state, we may read all the required properties into an array in one pass and then be done with the original object.

This can be implemented with reflection, method handles, or individual lambdas by iterating over a collection of accessors and invoking them one by one. But this means repeating the accessor dispatch for each property. It would be better if we could do that dispatch just once.

Built-in JDK solutions: so close, yet …​

With the above in mind — none of the solutions fulfill the needs:

  • The LambdaMetafactory can give us great performance for method calls and constructor invocations, but does not support direct field access.

  • None can give us an optimised multi-value access — reading or writing the entire tuple of entity properties in one go when draining the database resultsets into entities.

This leaves us with one more option to explore — generating the bytecode ourselves.

One factory to access them all

With the access patterns used across our projects and the shortcomings of built-in solutions in mind, we settled on the following simple API. It supports both single- and multi-value access, whether the underlying members are fields or methods:

/**
 * Creates new instances of a type by invoking a constructor.
 */
public interface Instantiator<T> {
    T create(Object... args);
}

/**
 * Reads a value from a field or getter method on an object instance.
 */
public interface ValueReader<T> {
    T get(Object instance);
}

/**
 * Writes a value to a field or setter method on an object instance.
 */
public interface ValueWriter {
    void set(Object instance, Object value);
}

/**
 * Reads multiple values from an object instance in a single call.
 */
public interface MultiValueReader {
    Object[] get(Object instance);
}

/**
 * Writes multiple values to an object instance in a single call.
 */
public interface MultiValueWriter {
    void set(Object instance, Object[] values);
}

/**
 * Main entry point to create accessors.
 */
public interface AccessorFactory {
    <T> Instantiator<T> instantiator(Constructor<T> constructor);

    ValueReader<?> valueReader(Field field);
    ValueReader<?> valueReader(Method getter);

    ValueWriter valueWriter(Field field);
    ValueWriter valueWriter(Method setter);

    MultiValueReader multiValueReader(Class<?> declaringClass, Member... members);
    MultiValueWriter multiValueWriter(Class<?> declaringClass, Member... members);
}

We can plug in different implementation strategies through an AccessorFactory, be it reflection-based or bytecode-generation-based, and use the resulting accessors to e.g. read object state:

AccessorFactory factory = AccessorFactory.reflection(MethodHandles.lookup());
ValueReader<?> reader = factory.valueReader(Person.class.getDeclaredField("name"));
Object name = reader.get(person);

This ultimately gives us a way to swap the underlying implementation based on the specific environment we are running in, or if the state of built-in solutions changes.

Taking matters into our own bytecode

That is how we arrived at the idea of generating the bytecode to get the access to the state. It would not be something entirely new, it has been done in the past, and we do already generate bytecode enhanced entities in Hibernate ORM. But this time we tried to make it more suitable for the whole Hibernate portfolio with specific access patterns of each project in mind.

Beyond generating direct member access, we also explored how to structure the access instructions within the generated classes/methods, including sharing one generated class across multiple accessors with switch-based dispatch. We will look at the tradeoffs of these approaches in the next post.

On paper generating some simple GETFIELD/PUTFIELD instructions is trivial. But creating a general-purpose, runtime-generated accessor library for the entire portfolio introduces a whole new set of modern JVM surprises and challenges:

  • How do you generate and define classes at runtime without exposing privileged access or bypassing module boundaries?

  • How can you get your hands on non-public properties?

  • How do you navigate the module boundaries under the JPMS?

  • How do you ensure runtime-generated classes don’t cause Metaspace leaks?

  • And why can defining a hidden class still fail even when you start with a fully privileged lookup?!??

We will dive into this and more in the second post of this series. Stay tuned!


Back to top