This experiment intends to demonstrate the power of the invokedynamic
instruction in JVM.
This instruction is used for different pieces of Java functionality these days, such as switch statements,
lambdas, and many cases of String concatenation.
The idea is that, in the bytecode of our compiled class, invokedynamic
points to some specific Bootstrap Method
,
which at runtime can choose a target method to be invoked. This way, we can have compact bytecode without even
knowing, for example, which method concatenates Strings for us. If newer Java versions improve performance
or add more alternative methods to choose from, we won’t need to do anything to benefit from them.
Redefining the Bootstrap method
Can we redefine the Bootstrap Method and invoke another target method at runtime? My experiment proves that it’s possible, although clearly, we shouldn’t ever want to do this :). It’s not intended for any real-life situations.
The case of String concatenation
As I mentioned, String concatenation is widely leveraged by invokedynamic
. Depending on the number of parts
to concatenate and their types, the behaviour can differ.
Let’s consider the following example:
When we compile this class and check the bytecode, we see the invokedynamic
instruction in the Code
section of our main
method:
In the BootstrapMethods
section, we see which method exactly is responsible for determining the dynamic behaviour:
StringConcatFactory.makeConcatWithConstants
.
Without going into too much detail, the functionality triggered by this method involves checking some parameters
and their types. It then looks up the MethodHandle
of a specific method and wraps it in a CallSite
instance.
Then, the found method is invoked via the JVM mechanisms.
StringConcatFactory
is all about this bootstrapping logic. Most of the target methods it finds in the
end are in StringConcatHelper
class.
For example, this is the body of method simpleConcat
, which will be called for our specific use case
(for concatenating a constant String
and a variable of non-primitive type):
It looks up the method StringConcatHelper::simpleConcat
, which accepts two Object
arguments,
and returns a String
. At the moment when the target method is invoked, there will be two Objects
sitting on top of our operand stack. We can also cheat a bit because we know that, in our case,
these Objects will be Strings.
How do we use this knowledge?
Can we override the body of this method and look up some other MethodHandle
? Of course we can! But we should keep
in mind that the signatures should match. Or, if they don’t match, we can manually inject some extra arguments.
I thought it would be fun to look up the String::replaceAll
method. Unlike the original method that is called,
it’s not a static method. Both of our Strings will come in as method arguments for the original method.
However, for replaceAll
, the first String will be the owner Object reference, and the second one will come
in as the first method argument (replace what). We are missing the second argument (replacement), but we can
inject this one ourselves.
Looking up some other MethodHandle
This code does the job:
We can test what this does with a simple example:
As a result, it will replace all “aaa”
in “rrrrr aaa fffff”
with cute smileys “^_^”
,
so we will end up with “rrrrr ^_^ fffff”
.
The bytecode
To override the method in StringConcatFactory
class, we will need to manipulate the bytecode.
The above code that we want to use as the new body for simpleConcat
method results in this bytecode:
The plan
To achieve my goal, I need to:
- Create a Java Agent that redefines the Bootstrap Method. When running a test program, we can simply specify a
javaagent
that we want to hook into the JVM. The Java Agent then does its dirty job before the invocation of themain
method of our test program. - Java 21 has a beautiful (internal) Class-File API for manipulating bytecode. Pretty cool thing, by the way! I’m looking forward to it coming out. We will try and use it to override a method body.
Simplest Java agent
Java Agent
s and Instrumentation
give a lot of possibilities, such as transformation of classes when they are
loaded, access to ClassLoader
information and many more.
We will make a very simple Java Agent. We will create a class that implements this method:
This method, as the name suggests, is triggered before the main
method of our test program.
Once the implementation is done, we will specify some settings in the manifest file and create a jar
package.
Finally, we can use the jar
package as a javaagent
when we run the JVM.
Alternatively, we could have created a class transformer and added it via Instrumentation
:
Then, our class transformer BootstrapStringConcatTransformer
would be triggered for
all classes that get loaded. However, our target class is not loaded before runtime because,
before runtime, the JVM doesn’t even know what invokedynamic
will need! So, we will need to load
the class of interest ourselves and then redefine it.
Class-File API
There aren’t a lot of docs for the Class-File API yet.
This API is not exposed to the world yet, but it’s interesting to try it out!
This is the only good example of usage that I found.
To be able to use it, we have to add the --add-exports
option to use the closed jdk.internal.*
packages.
After playing with it a bit, I got to the following code, which is not polished, but it works:
Packaging the agent
I compiled the Java Agent code with these options:
Then, I created the manifest with the following settings:
And packaged a jar
:
Testing the result
For reference, this is our test program again:
Let’s test our program without javaagent
using the java Test
command. This is the result:
Now, let’s attach our javaagent
(unfortunately, we need to keep all the package opening options for it to work):
Here’s the result:
The concatenation is no longer concatenating 😛.
Final Thoughts
Would have been fun to replace the target method with some custom method, right? Unfortunately,
that didn’t work out for me. I tried providing my custom method packaged in a jar
through Instrumentation
(yes, it’s also possible) so it’s available for bootstrap ClassLoader
.
This code would be accessible by any code in my Test program. However, JDK internal code isn’t allowed to access the code I provide. If you know the options to “make it allowed,” please let me know.
I have also tried to add an extra method in the StringConcatHelper
class, but it’s impossible to
add any methods or change the signatures of existing ones. The reason is that the class is already loaded now,
and the class data is stored in appropriate memory slots.
You may be wondering, why did I try this in the first place? Because, why not? Thank you for your attention, and stay tuned for the next JVM torturing episodes!