Running on Java 24-ea+25-3155 (Preview)
Home of The JavaSpecialists' Newsletter

287@PolymorphicSignature

Author: Dr Heinz M. KabutzDate: 2021-01-31Java Version: 1.7+Category: Performance
 

Abstract: MethodHandles annotate the invoke() methods with @PolymorphicSignature. In this newsletter we see how this can help to avoid unnecessary object creation.

 

Welcome to the 287th edition of The Java(tm) Specialists' Newsletter, sent to you from the beautiful Island of Crete. It is sunny out and I hope to put the finishing touches to this newsletter before soaking up Vitamin D splitting yucca elephantipes logs. When we moved into our home, we were amazed how easily these yuccas would grow. Our farmer neighbor shook his head as we enthusiastically shoved seedlings into every available patch of ground. A few years later they had almost reached the roof of our two story house. Turns out they are not resistent against Stihl :-) Our local gardening store told us that the wood can be burned once it has dried out, so to speed things up, I thought I'd do a map/reduce on them. I guess the "elephantipes" should have been a clue how big they would become, had I bothered to check the Latin.

About a year ago my son and I were sitting in my cousin's house in Munich, chatting to her orthopeadic surgeon husband. My son Maxi and I were in high spirits, looking forward to a Dream Theater concert the next evening. The timing was perfect. Sunday night our favourite band, and then on Monday morning I would teach my favourite course - Java Concurrency. I had not traveled with my son for years and this was a real treat. Achim yanked me back to reality by asking my opinion about this new corona virus. It had recently surfaced in their next town and he was concerned. What did I think? Having seen SARS-CoV-1 come and go, I was hopeful that this would be much of the same. That Java concurrency course was the last classroom training I would teach in a while.

We began investing in remote teaching in 2010, both as self-study and as instructor-led live classes. For a decade, we encouraged our customers to try the remote options. Some did, but most wanted an instructor to come to their location. Heinz: "We can save 12.5% of costs by not paying for travel" - Customer: "We don't care". "We can save the planet by reducing our carbon footprint" - "Let it burn" (OK, that was an exaggeration.) Remote teaching was a hard sell.

All that changed because of a tiny virus. Our world is slowly moving towards the acceptance phase. Remote is the new normal. At the beginning of the pandemic, companies were hesitant to train their programmers via remote learning. Their objections were mostly based on some bad experiences, plus the hope that we could soon go back to physical classes. The more time goes by, the more we are getting used to this. We are all improving at delivering and consuming remote content. For example, my kids get several gym sessions a week with Nicholas Ingel, training them from South Africa via Zoom (Highly recommended - great instruction and a super motivator). I attend a weekly Bible study with Bill Creasy, also via Zoom (Again, fantastic content - also on Audible).

2021 is going to be a great year for remote learning and I am excited to be part of it. You can find my training as self-study on my JavaSpecialists.Teachable.com school, with large lecture halls of live classes at O'Reilly's Learning Platform and of course with in-house corporate classes on Java Specialists Training.

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

@PolymorphicSignature

We had an interesting discussion on our JavaSpecialists Slack Team about MethodHandles vs reflection. Paul Sandoz wrote an excellent article in 2014 called Deconstructing MethodHandles. I do not wish to repeat what he wrote, but want to focus on one aspect that I find quite cool - how MethodHandles avoid unnecessary object creation.

One of the advantages that MethodHandle has over Method is that arguments and return types do not need to be boxed to the wrapper types. Thus if your parameter is an int, it can remain that and never needs to be converted to an Integer. The MethodHandle#invokeExact method is marked with the annotation @PolymorphicSignature, which gives the JVM more permission to be creative about optimizing the method signature. Even though an int is not an Object, with the @PolymorphicSignature annotation it can always stay the primitive type.

Escape analysis is great at getting rid of unnecessary object creation on the Java heap. However, in some cases it will not be able to determine that an object cannot escape, and thus we would still need to pay the penalty of collecting the garbage. I saw this during some of the benchmarks for my book on Dynamic Proxies in Java. It would thus be safer to avoid unnecessary object creation where possible.

For this experiment, I used the static Short#compare(short,short) method. Primitives in the range -128 to 127 are boxed to objects from a shared cache. Since short has a range of -32768 to 32767, we can box short primitives to fall outside that range. This would not be the case with byte. Furthermore, the compare() method for Short returns x - y. Thus if we say Short.compare(127,-128), then the result is 255, again falling outside of the guaranteed boxed cache range. This lets us create different variations of arguments and results in order to try out whether boxing occurs or not.

The code should mostly be self-explanatory. We have six experiments, three for java.lang.reflect.Method and another three for java.lang.invoke.MethodHandle. When we run the code, we should disable escape analysis with -XX:-DoEscapeAnalysis and enable GC logging with -verbose:gc.

import java.lang.invoke.*;
import java.lang.reflect.*;

/**
 * In this experiment, we look at the effects of the annotation
 * {@link java.lang.invoke.MethodHandle.PolymorphicSignature}.
 * We will invoke {@link java.lang.Short#compare(short, short)},
 * which takes primitive arguments and returns a primitive. Will
 * these have to be boxed? What about the parameter array? Will
 * that need to be created? With Short#compare we can have
 * parameters and return values that fall outside of Short's
 * boxing cache. The compare() method returns "x - y", whereas
 * Integer and Long both return 0, 1, or -1, to avoid overflow.
 *
 * Reflection wraps exceptions as InvocationTargetException,
 * whereas MethodHandle.invokeExact() throws the original
 * exception as is. Thus, we need to throw Throwable.
 *
 * To see the effects of the PolymorphicSignature, please use the
 * VM parameters -XX:-DoEscapeAnalysis -verbose:gc.
 *
 * @author Dr Heinz M. Kabutz
 */
public class MethodHandlesGarbage {
  public static void main(String... args) throws Throwable {
    reflectiveMethodWithFullCaching();
    reflectiveMethodWithCachedParameterArray();
    reflectiveMethodWithCachedIntegerResult();

    methodHandle1();
    methodHandle2();
    methodHandle3();
  }

  /**
   * The parameter array is cached in our method and the result
   * is 90, thus within the range of the Short boxed cache. We
   * would not expect any Short objects to be created.
   */
  private static void reflectiveMethodWithFullCaching()
      throws Throwable {
    doExperiment(() -> {
      Short[] args = {50, -40};
      for (int i = 0; i < 100_000_000; i++) {
        int result = (int) compareMethod.invoke(null, args);
      }
    });
  }

  /**
   * The parameter array is cached, but the result is 255, thus
   * not in the Short boxed cache. We would expect Short objects
   * to be created in the absence of escape analysis.
   */
  private static void reflectiveMethodWithCachedParameterArray()
      throws Throwable {
    doExperiment(() -> {
      Short[] args = {127, -128};
      for (int i = 0; i < 100_000_000; i++) {
        int result = (int) compareMethod.invoke(null, args);
      }
    });
  }

  /**
   * The parameter array is not cached, thus it needs to be
   * created every time. All the boxed Short instances come from
   * the Short cache. Note that escape analysis is generally
   * good at eliminating this parameter array, but we have seen
   * instances where it was not able to, most notably when using
   * dynamic proxies and with deep call stacks.
   */
  private static void reflectiveMethodWithCachedIntegerResult()
      throws Throwable {
    doExperiment(() -> {
      for (int i = 0; i < 100_000_000; i++) {
        short x = 50, y = -40;
        int result = (int) compareMethod.invoke(null, x, y);
      }
    });
  }

  /**
   * The parameters fall within the range of the Short boxed
   * cache, but the result does not. Despite that, the result
   * is not boxed / unboxed, as we can see in the GC logs.
   * Note that even though the signature of invokeExact() has
   * varargs (...), no array is created either.
   */
  private static void methodHandle1()
      throws Throwable {
    doExperiment(() -> {
      for (int i = 0; i < 100_000_000; i++) {
        short x = 127, y = -128;
        int result = (int) compareMH.invokeExact(x, y);
      }
    });
  }

  /**
   * The parameters call outside the range of the Short boxed
   * cache, but the result falls inside (-10). Still, no objects
   * are created.
   */
  private static void methodHandle2()
      throws Throwable {
    doExperiment(() -> {
      for (int i = 0; i < 100_000_000; i++) {
        short x = 1000, y = 1010;
        int result = (int) compareMH.invokeExact(x, y);
      }
    });
  }

  /**
   * Both the parameters and the result fall inside the Short
   * boxed cache. No parameter array is created for the method
   * call.
   */
  private static void methodHandle3()
      throws Throwable {
    doExperiment(() -> {
      for (int i = 0; i < 100_000_000; i++) {
        short x = 50, y = -40;
        int result = (int) compareMH.invokeExact(x, y);
      }
    });
  }

  private static void doExperiment(Experiment experiment)
      throws Throwable {
    printCallerMethod();
    System.gc();
    experiment.run();
    System.gc();
    System.out.println();
  }

  private static void printCallerMethod() {
    StackWalker.getInstance().walk(s -> s.skip(2)
        .map(StackWalker.StackFrame::getMethodName)
        .findFirst())
        .ifPresent(method -> System.out.println(method + "()"));
  }

  @FunctionalInterface
  private static interface Experiment {
    void run() throws Throwable;
  }

  private static final Method compareMethod;
  private static final MethodHandle compareMH;

  static {
    try {
      compareMethod = Short.class.getMethod("compare",
          short.class, short.class);
      compareMH = MethodHandles.lookup().findStatic(Short.class,
          "compare", MethodType.methodType(
              int.class, short.class, short.class));
    } catch (ReflectiveOperationException e) {
      throw new Error(e);
    }
  }
}

When we run the code, we see the following output (slightly trimmed to fit into the available space):

reflectiveMethodWithFullCaching()
[0.106s] GC(0) Pause Full (System.gc()) 12M->8M(80M) 14.937ms
[0.571s] GC(1) Pause Full (System.gc()) 9M->8M(80M) 3.446ms

reflectiveMethodWithCachedParameterArray()
[0.574s] GC(2) Pause Full (System.gc()) 8M->8M(80M) 2.385ms
[0.607s] GC(3) Pause Young (Normal) 24M->8M(80M) 0.337ms
[0.617s] GC(4) Pause Young (Normal) 32M->8M(80M) 0.342ms
[0.630s] GC(5) Pause Young (Normal) 40M->8M(80M) 0.201ms
[0.640s] GC(6) Pause Young (Normal) 40M->8M(80M) 0.178ms
[0.650s] GC(7) Pause Young (Normal) 40M->8M(552M) 0.723ms
[0.880s] GC(8) Pause Young (Normal) 328M->8M(552M) 0.611ms
[0.991s] GC(9) Pause Young (Normal) 328M->8M(552M) 0.529ms
[1.090s] GC(10) Pause Young (Normal) 328M->8M(552M) 0.551ms
[1.191s] GC(11) Pause Young (Normal) 328M->8M(552M) 1.817ms
[1.233s] GC(12) Pause Full (System.gc()) 124M->8M(112M) 5.079ms

reflectiveMethodWithCachedIntegerResult()
[1.248s] GC(13) Pause Full (System.gc()) 10M->8M(80M) 4.107ms
[1.302s] GC(14) Pause Young (Normal) 48M->8M(80M) 0.246ms
[1.314s] GC(15) Pause Young (Normal) 48M->8M(80M) 0.340ms
[1.326s] GC(16) Pause Young (Normal) 48M->8M(80M) 0.216ms
[1.338s] GC(17) Pause Young (Normal) 48M->8M(552M) 0.727ms
[1.573s] GC(18) Pause Young (Normal) 336M->8M(552M) 0.443ms
[1.669s] GC(19) Pause Young (Normal) 336M->8M(552M) 1.069ms
[1.757s] GC(20) Pause Young (Normal) 336M->8M(552M) 0.951ms
[1.848s] GC(21) Pause Young (Normal) 336M->8M(552M) 0.458ms
[1.940s] GC(22) Pause Young (Normal) 336M->8M(552M) 0.408ms
[2.032s] GC(23) Pause Young (Normal) 336M->8M(552M) 0.445ms
[2.082s] GC(24) Pause Full (System.gc()) 172M->8M(80M) 6.056ms

methodHandle1()
[2.094s] GC(25) Pause Full (System.gc()) 9M->8M(80M) 3.364ms
[2.135s] GC(26) Pause Full (System.gc()) 9M->8M(80M) 3.405ms

methodHandle2()
[2.139s] GC(27) Pause Full (System.gc()) 9M->8M(80M) 3.015ms
[2.173s] GC(28) Pause Full (System.gc()) 8M->8M(80M) 3.537ms

methodHandle3()
[2.176s] GC(29) Pause Full (System.gc()) 9M->8M(80M) 2.826ms
[2.207s] GC(30) Pause Full (System.gc()) 8M->8M(80M) 3.187ms

As we see, the methods that create objects are reflectiveMethodWithCachedParameterArray() and reflectiveMethodWithCachedIntegerResult(), creating an Integer object for the return type and an Object[] for the parameter list respectively.

Yes, these can be optimized away in most cases. But not in all. There are some other advantages of using the MethodHandle. For example, security is checked when the MethodHandle is created, rather than every time that invokeExact() is called. If an exception occurs, the actual exception is thrown and not wrapped with an InvocationTargetException. Stack traces are expensive to create. However, in this newsletter, I just wanted to focus on the one aspect of MethodHandles, that of the @PolymorphicSignature.

Kind regards

Heinz

P.S. Sun is still out, so I better get started on the chopping if I want to catch a few moments of Vit-D.

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '24 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...