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.
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.
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)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.