Explaining invokedynamic. Java 9 String concatenation. Part VI

This is sixth part of mini-series of Explaining invokedynamic. This is the full list of all articles:

Introduction. Part I

Toy example. Part II

Bootstrap method . Part III

Number multiplication (almost) complete example. Part IV

Dynamical hashCode implementation. Part V

Java 9 String concatenation. Part VI

Lambda. Part VII

Records. Part VII

Final notes. Part IX

On the previous part we have dive into implementation logic by MethodHandle API.

Here we will look on big picture again. We will look on how this was implemented in JDK 9.

Java 9 String concatenation

Big Picture

Before diving into details of how this new approach works, let’s see it from a broader point of view.

As an example, suppose we’re going to create a new String by joining another String with an String. We can think of this as a function that accepts a String and an Stringand then returns the concatenated String.

Here’s how the new approach works for this example:

* Preparing the function signature describing the concatenation. For instance, (String, String) -> String.

* Preparing the actual arguments for the concatenation. For instance, if we’re going to join “The answer is“ and “ 42”, then these values will be the arguments.

* Calling the bootstrap method and passing the function signature, the arguments, and a few other parameters to it.

* Generating the actual implementation for that function signature and encapsulating it inside a MethodHandle.

* Calling the generated function to create the final joined string.

(String, int) -> String function should be (String, String) -> String

Put simply, the bytecode defines a specification at compile-time. Then the bootstrap method links an implementation to that specification at runtime. This, in turn, will make it possible to change the implementation without touching the bytecode.

https://www.baeldung.com/java-string-concatenation-invoke-dynamic

Prior JDK 1.5 (currently known as JDK 5) non-trivial string concatenations were implemented using StringBuffer. Quote from JavaDoc:

As of release JDK 5, this class has been supplemented with an equivalent class designed for use by a single thread, StringBuilder. The StringBuilder class should generally be used in preference to this one, as it supports all of the same operations but it is faster, as it performs no synchronization.

https://docs.oracle.com/javase/9/docs/api/java/lang/StringBuffer.html

Let’s look on concrete example.

Compiler generated byte-code that is equivalent to the following:

  • Prior JDK 1.5 (currently known as JDK 5):
  • From JDK 5 till JDK 9:
  • From JDK 9 onward

using invokedynamic bytecode with customizable bootstrap method.

As of Java 9 and as part of JEP 280, the string concatenation is now using invokedynamic.

The primary motivation behind the change is to have a more dynamic implementation. That is, it’s possible to change the concatenation strategy without changing the bytecode. This way, clients can benefit from a new optimized strategy even without recompilation.

There are other advantages, too. For example, the bytecode for invokedynamic is more elegant, less brittle, and smaller…

…Similar to other invoke dynamic implementations, much of the logic is moved out from compile-time to runtime.

https://www.baeldung.com/java-string-concatenation-invoke-dynamic

Note:

It’s possible to travel back to the pre-Java 9 world and use StringBuilder with the -XDstringConcat=inline compiler option. This means no invokedynamic bytecode generation. Compiler will emit bytecode equivalent to using StringBuilder.

By default, the bootstrap method is StringConcatFactory.makeConcatWithConstants​ also known as indy with constants.

Note:

There is also another flavor — StringConcatFactory.makeConcat. It generate a bit different variant of bytecode. It doesn’t differentiate between constant and dynamic parts and passes all of them as arguments. In our example, firstName and lastName are dynamic part and all the rest Strings are static, so they can be passed through constant pool (that was default indyWithConstants, the default one) does. I will ignore this flavor.

Let’s see, what is going on in Java 9 with default settings:

When the JVM sees the invokedynamic instruction for the first time, it calls the makeConcatWithConstants bootstrap method. The bootstrap method will, in turn, return a ConstantCallSite, which points to the concatenation logic.

Among the arguments passed to the bootstrap method, two stand out:

Ljava/lang/invoke/MethodType represents the string concatenation signature. In this case, it’s (LString;I)LString since we’re combining an integer with a String

\u0001\u0001 is the recipe for constructing the string (more on this later).

https://www.baeldung.com/java-string-concatenation-invoke-dynamic

To generate a String representation, the JVM passes firstName and lastName fields to the invokedynamic instruction as the arguments:

Quote (without original formatting):

As shown above, the recipe represents the basic structure of the concatenated String. For instance, the preceding recipe consists of:

Constant strings such as “Person“. These literal values will be present in the concatenated string as-is

Two \u0001 tags to represent ordinary arguments. They will be replaced by the actual arguments such as firstName

We can think of the recipe as a templated String containing both static parts and variable placeholders.

Using recipes can dramatically reduce the number of arguments passed to the bootstrap method, as we only need to pass all dynamic arguments plus one recipe.

https://www.baeldung.com/java-string-concatenation-invoke-dynamic

By default, the strategy calculates the required storage upfront and directly maintains its byte[] to store the concatenation result.

What is really great in the whole story, that you can use all defaults to generate invokedynamicbytecode. It will use StringConcatFactory.makeConcat as bootstraping method. By default it would use default strategy that is described above.

Then, you can change string concatenation strategy without changing bytecode.

The bootstrap method eventually provides a MethodHandlethat points to the actual concatenation logic. As of this writing, there are six different strategies to generate this logic:

BC_SB or “bytecode StringBuilder” strategy generates the same StringBuilder bytecode at runtime. Then it loads the generated bytecode via the Unsafe.defineAnonymousClass method

BC_SB_SIZED strategy will try to guess the necessary capacity for StringBuilder. Other than that, it’s identical to the previous approach. Guessing the capacity can potentially help the StringBuilder to perform the concatenation without resizing the underlying byte[]

BC_SB_SIZED_EXACT is a bytecode generator based on StringBuilder that computes the required storage exactly. To calculate the exact size, first, it converts all arguments to String

MH_SB_SIZED is based on MethodHandles and eventually calls the StringBuilder API for concatenation. This strategy also makes an educated guess about the required capacity

MH_SB_SIZED_EXACT is similar to the previous one except it calculates the necessary capacity with complete accuracy

MH_INLINE_SIZE_EXACT calculates the required storage upfront and directly maintains its byte[] to store the concatenation result. This strategy is inline because it replicates what StringBuilder does internally

The default strategy is MH_INLINE_SIZE_EXACT. However, we can change this strategy using the -Djava.lang.invoke.stringConcat=<strategyName> system property.

https://www.baeldung.com/java-string-concatenation-invoke-dynamic

As far as compiler concern, invokedynamic just links the call site with bootstrap method (StringConcatFactory.makeConcat by default) and passes all necessary argument to calculate result. This part is static.

Then, in runtime you can change the behavior of bootstrap method by supplying system property to the JVM when you run the code.

For instance, you can compile your code using defaults, deploy it to production, and in production change the strategy to another one without code recompilation.

Even more, in Java 21, new strategy can be added to JVM. You can switch to new strategy without changing any generated bytecode of your class. This strategy maybe something completely unknown today. Class that you’ve wrote may be in maintenance mode, and still you can improve it’s performance by changing string concatenation strategy (or it can be done by JVM itself, in new Java version default string concatenation strategy may be changed as it did in Java 5 and Java 9 in the past).

Senior Software Engineer at Pursway

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store