Monday, May 8, 2023

The performance implications of Java reflection

Reflection slows down your Java code. Why is that?


Reflection is powerful—and often misunderstood. This article will build on the introduction of the Core Reflection API introduced in “Reflection for the modern Java programmer” and discuss two major additional topics: how reflection is implemented in the HotSpot JVM and the changes made to reflection in recent versions of the Java platform.

Oracle Java Career, Java Skills, Java Jobs, Java Tutorial and Materials, Java Guides, Java Learning

This discussion begins by exploring a simplified form of the reflection mechanism’s code from the JDK. The code in the following examples resembles the implementation in Java 8, but some complexity has been removed for clarity. This basic version is used in all but the most recent Java versions. (A future article will look at the latest changes.)

Start by looking at the invoke() method on Method, which looks like the following:

public Object invoke(Object obj, Object… args)
    throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException {

  if (!override) {
    if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
      Class<?> caller = Reflection.getCallerClass();
      checkAccess(caller, clazz, obj, modifiers);
    }
  }
  MethodAccessor ma = methodAccessor;
  if (ma == null) {
    ma = acquireMethodAccessor();
  }
  return ma.invoke(obj, args);
}

The code first checks to see if the override flag has been set; it will have been set if setAccessible() has been called. Next, a reference to a MethodAccessor object is obtained, and then the invoke() call is delegated to it.

(Note: Many of the classes in this section are not in an API package of java.base, so they cannot be called directly in modern Java code. For example, MethodAccessor is in jdk.internal.reflect.)

The MethodAccessor interface is the key to the reflective invocation capability. It acts as a straightforward delegate.

public interface MethodAccessor {
    /** Matches specification in {@link java.lang.reflect.Method} */
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException;
}

The first time this code is called, the method acquireMethodAccessor() creates an instance of the type DelegatingMethodAccessorImpl that implements MethodAccessor, as follows:

import java.lang.reflect.InvocationTargetException;

/** Delegates its invocation to another MethodAccessorImpl and can
    change its delegate at runtime. */

class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
  private MethodAccessorImpl delegate;

  DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
    setDelegate(delegate);
  }

  public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException {
    return delegate.invoke(obj, args);
  }

  void setDelegate(MethodAccessorImpl delegate) {
    this.delegate = delegate;
  }
}

As the comment explains, the purpose of this class is to act as an appropriate amount of indirection and provide a delegation point that can be updated at runtime. The initial delegate is an instance of the class NativeMethodAccessorImpl.

class NativeMethodAccessorImpl extends MethodAccessorImpl {
  private Method method;
  private DelegatingMethodAccessorImpl parent;
  private int numInvocations;

  // ...

  public Object invoke(Object obj, Object[] args)
          throws IllegalArgumentException, InvocationTargetException {

    if (++numInvocations >
          ReflectionFactory.inflationThreshold()) {
      MethodAccessorImpl acc = (MethodAccessorImpl)
          new MethodAccessorGenerator()
            .generateMethod(method.getDeclaringClass(),
                            method.getName(),
                            method.getParameterTypes(),
                            method.getReturnType(),
                            method.getExceptionTypes(),
                            method.getModifiers());
        parent.setDelegate(acc);
    }

    return invoke0(method, obj, args);
  }

  private static native Object invoke0(Method m, Object obj, Object[] args);

  // ...
}

This code contains an if block that will be entered after an invocation threshold is reached, such as after the reflective method has been called a certain number of times. If the invocation threshold has not yet been reached, the code proceeds with the native call.

Once the threshold has been reached, NativeMethodAccessorImpl will use a code generation factory, contained in MethodAccessorGenerator.generateMethod(), to create a custom class that contains bytecode that calls the target of the reflective call.

After creating an instance of this dynamically created class, the call to setDelegate() uses an uplevel reference to the parent accessor to replace the current object with acc, the newly created custom object.

For technical reasons related to class verification, the JVM must be aware of the special nature of the reflective accessor classes. For this reason, there is a special accessor class in the inheritance hierarchy that acts as a marker for the JVM. The precise details of this need not concern you, so don’t worry.

Overall, the mechanism as described represents a performance trade-off—some reflective calls are made only a few times, so the code generation process could be very expensive or wasteful. On the other hand, switching from Java into a native call is slower than remaining in pure Java. This approach allows the runtime to avoid code generation until it seems likely that the reflective call will be made relatively often.

As a result, the costs of code generation can then be amortized over the lifetime of the program, while still providing better performance for later calls than the native implementation can achieve.

Reflecting on reflection


You can see more of the reflection subsystem in action (and some recent changes) by reflecting on the subsystem itself. Assume the class has the following two methods on it—one is a simple method that just prints a message, and one is a reflective accessor for it:

public static void printStr() {
    System.out.println("Hello world");
}

public Method getMethodObj() throws NoSuchMethodException {
    var selfClazz = getClass();
    var toStr = selfClazz.getMethod("printStr");
    return toStr;
}

Additionally, there’s the following calling code (exception handling has been elided for clarity):

var m = self.getMethodObj();
Class<?> mClazz = m.getClass();
System.out.println(mClazz);
// This is necessary due to some aspects of lazy evaluation
m.invoke(null);

var f = mClazz.getDeclaredField("methodAccessor");
f.setAccessible(true);
Object ma = f.get(m);
System.out.println(ma.getClass());

The code above produces the following output when run with Java 11:

$ java javamag.reflection.ex2.ReflectTheReflect
class java.lang.reflect.Method
Hello world
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by javamag.reflection.ex2.ReflectTheReflect (file:/Users/ben/projects/writing/Oracle/Articles/reflection/src/main/java/) to field java.lang.reflect.Method.methodAccessor
WARNING: Please consider reporting this to the maintainers of javamag.reflection.ex2.ReflectTheReflect
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
class jdk.internal.reflect.DelegatingMethodAccessorImpl

There are two things to be aware of here.

◉ First, the initial reflective invocation of the method is required. If that initial invocation is omitted, the code will fail. This is due to lazy initialization of the reflection subsystem: For performance reasons, the Method objects are not populated unless they are needed.
◉ Second, if you now switch to Java 17, the code will fail with the following output:

$ java javamag.reflection.ex2.ReflectTheReflect
class java.lang.reflect.Method
Hello world
java.lang.NoSuchFieldException: methodAccessor
       at java.base/java.lang.Class.getDeclaredField(Class.java:2610)
       at javamag.reflection.ex2.ReflectTheReflect.main(ReflectTheReflect.java:15)

The failure happens because of the changes in visibility, access control, and reflection, as discussed in “A peek into Java 17: Encapsulating the Java runtime internals.” You can no longer assume that reflection allows you to poke around inside the platform without limits.

How reflection affects performance


It’s easy to imagine that the flexibility of reflection comes at a price in terms of runtime performance. The obvious, immediate question that comes to mind is this: “How big is that cost?” However, this question carries a hidden assumption—that the question is meaningful and well-formed in the first place. It’s not always that easy.

You can, of course, write a Java Microbenchmark Harness (JMH) benchmark that compares a reflective call to a direct one. Such a benchmark could look a bit like the following. The JMH annotations can be found in the package org.openjdk.jmh.annotations.

@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class SimpleReflectionBench {

    private static Method getTime = null;
    private static Object o = null;

    static {
        try {
            var clazz = ReflectionHolder.class;
            var ctor = clazz.getConstructor();
            o = ctor.newInstance();
            getTime = clazz.getMethod("getTime");
        } catch (Exception x) {
            throw new RuntimeException(x);
        }
    }

    @Benchmark
    public long runReflective() throws InvocationTargetException, IllegalAccessException {
        Object ret = getTime.invoke(o);
        return (long)ret;
    }

    @Benchmark
    public long runDirect() {
        var opt = (ReflectionHolder)o;
        return opt.getTime();
    }

    static class ReflectionHolder {
        public ReflectionHolder() {}

        public long getTime() {
            return System.currentTimeMillis();
        }
    }
}

The details will vary a bit depending on which hardware platform you are using, but a typical result will be on the order of a 23% hit to performance.

But no real Java program consists of just a single call. In reality, this code runs within an application process (the JVM), and you can’t easily isolate the performance of the executing application code from the JIT compiler, memory management, and other runtime subsystems present in that application process.

In addition, the JIT compiler performs heavy optimization and transformation of the program—and the precise details of how the program is transformed depend very much on the program.

For example, one of the most powerful transformations that the JIT compiler performs is automatic method inlining. This is, in fact, one of the first transformations to be carried out, because combining method bodies brings more code into the view of the JIT compiler. This potentially allows optimizations that would not have been possible if the JIT compiler could look only at the code of a single method at a time.

Unfortunately, reflective calls are not typically inlined in the general case, due to their dynamic nature. Note the word typically, as there are caveats that apply to this statement.

As you’ve already seen, the reflection implementation generates Java bytecode (using MethodAccessorGenerator.generateMethod()) for the call. This can make it easier for the JIT compiler to inline the reflective call if certain conditions hold, such as the Method object being rooted in a static final field and the target method being static (or having a definitely known receiver type).

Overall, this means that the actual overhead of reflective calls is not easy to reason about from first principles, but it can easily be much, much larger than 23% due to the operation of inlining on direct calls, which is not really possible for equivalent reflective calls.

The main conclusion this leads to is that you must question what a simplistic figure such as 23% means—or if it means anything at all.

To quote Brian Goetz, “The scary thing about microbenchmarks is that they always produce a number, even if that number is meaningless. They measure something; we’re just not sure what.”

The small-scale performance effects are effectively smoothed out by dealing with a larger aggregate (that is, the whole system or a subsystem), but that makes it very hard or impossible to make general, code-level recommendations for writing performant applications.

You can say that application performance is an emergent phenomenon, as it arises from the natural scale of your applications (potentially thousands of classes or millions of lines of code), and there is not necessarily any single root cause (or small set of root causes) for a specific gain or loss.

This fact is what leads performance professionals to urge developers to simply write good, clear code and let the runtime take care of optimization.

A performance tip is quite often just a workaround for some quirk of the runtime—and if application developers are aware of the quirk, it is quite likely that so are the JVM developers—and that they are working to fix it. To illustrate this, in the case of reflection, you can ask, “What JVM-level mechanisms are involved that could complicate the overall picture of the effect?”

Figure 1 shows a single line of Java code that executes a reflective call on a Method object denoted as M, with arguments x and y. It has been annotated to show the major aspects of runtime behavior that might have a performance impact on the execution of the call.

Oracle Java Career, Java Skills, Java Jobs, Java Tutorial and Materials, Java Guides, Java Learning
Figure 1. JVM effects on reflection

Here are four big areas that could affect performance.

◉ Boxing occurs at several different places.
◉ The call site for a reflective call is said to be megamorphic (many possible implementations for the method) because in the implementation up to Java 17, each instance of Method has a different, dynamically spun method accessor object.
◉ The methodAccessor field on Method is volatile, so rereading it is mandatory. Therefore, the invoke() call leads to an indirection and a virtual dispatch on the delegate.
◉ Method accessibility checks are performed on each call.

To dig a little deeper, examine the signature of the invoke() method on Method.

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

This is the most general signature for a Java method that you can write—and it has to be. Reflective calls could be of any signature, and that type information will not, in general, be available until runtime. Therefore, if Method represents the capability to call a method, and invoke() represents the actual call, it is entirely logical that the signature must be this general.

Java’s type system is not single-rooted, so any primitives that appear will be handled via boxing.

Separately, the accessibility check is important, because access control is normally checked at classloading time (and any poorly behaved classes that attempt to infringe on it are not loaded). Use of reflective code changes this picture, and the Core Reflection API has a couple of weaknesses (or necessary evils, depending on how you feel about them).

◉ You can get a reflection object corresponding to a method that you would not be able to call directly.
◉ You can break the rules of the Java language by allowing calling code to selectively disable access control using setAccessible().

Therefore, the implementation of Method must check whether the Method object requires an access control check on every call (and perform it, if so). It is necessary to do this on every call because the Method object may have had setAccessible() called on it after the previous call.

These accessibility checks impact performance, as you can see by modifying the benchmark as follows:

static {
        try {
            var clazz = ReflectionHolder.class;
            var ctor = clazz.getConstructor();
            o = ctor.newInstance();
            getTime = clazz.getMethod("getTime");
            // Disable access control checking
            getTime.setAccessible(true);
        } catch (Exception x) {
            throw new RuntimeException(x);
        }
    }

Comparing the results from this benchmark to the reflective benchmark in the previous case shows that a call that is not checked for access control seems to run faster. However, the overall aggregate effect is still unknown, so you must avoid making any dubious conclusions about the general usefulness of disabling access control for reflective calls.

As stated previously, the proper object of study for performance is the whole application. This suggests that you could, for example, recompile the JDK with a code change so that access control is always ignored. This would, in theory, give a better number for at least one aspect of reflective performance in aggregate.

However, if you did do this, can you really be sure that globally disabling reflective access control checks doesn’t change semantics anywhere in the application—or in the libraries that it depends upon? After all, perhaps a framework that you’re using has a clever strategy that probes possible methods for reflective invocation and relies on the access control semantics to improve performance.

If it did, how would you know?

Source: oracle.com


Related Posts

0 comments:

Post a Comment