VM Instrumentation Without an Agent
How to get java.lang.instrument.Instrumentation without -javaagent
VM Instrumentation Without an Agent
The Java Instrumentation API is typically accessed through the -javaagent command line flag, but what if you want instrumentation capabilities in an already running process? Today I'll show you how to create a fully functional Instrumentation object without ever using -javaagent.
The 'What' and 'Why' of Instrumentation
Before we dive into the native-side "how," let's quickly cover the "what" and "why."
So, what even is instrumentation? At its core, the java.lang.instrument API is a way to hook into the JVM's class-loading process. It lets you get the raw bytecode of a class before it's used, and you can change it. This is called "transforming." You can add logging, check for security stuff, or monitor performance, all without touching the original .java file.
This Java API is just a nice wrapper around a deeper, native layer. This is the JVMTI (JVM Tool Interface). To do the really cool stuff, like redefining classes that are already loaded, the native code needs to ask the JVM for permission. These permissions are called capabilities, like can_redefine_classes or can_retransform_classes.
This brings us to the whole point of this post. The standard -javaagent flag is great, but you have to specify it on the command line before the process starts. What if you want to attach a profiler or a security tool to a Java server that's already running and you can't restart it? This technique shows you how to bypass the normal startup requirements, build all the internal JVM structures by hand, and get a fully-powered Instrumentation object in a live process.
The Standard Approach
Normally, you'd get instrumentation like this
// Agent.java
Then run with
The JVM creates an Instrumentation instance and passes it to your premain method, but what actually happens under the hood?
JPLIS Internals
The Java programming language Instrumentation services (JPLIS) is the native component that implements the Instrumentation API. When you use -javaagent, the JVM loads your agent jar and creates a native JPLISAgent structure.
This structure lives in JPLISAgent.h and looks like this
// from JPLISAgent.h
;
The key insight is that sun.instrument.InstrumentationImpl (the concrete implementation of the Instrumentation interface) is just a regular Java class that wraps this native structure. In the Java class, this is stored in a long field
// from sun.instrument.InstrumentationImpl.java
// needs to store a native pointer, so use 64 bits
private final long mNativeAgent;
Its constructor signature is
// from sun.instrument.InstrumentationImpl.java
private
The first parameter is a pointer to the native JPLISAgent structure, cast to a long. The JVM doesn't actually validate that this pointer came from a legitimate agent loading process. it just stores it and uses it when you call Instrumentation methods.
Getting JVMTI Without an Agent
Before we can create a fake agent, we need JVMTI access. Normally this requires loading a native agent with -agentlib, but there's a simpler way.
From any native code running in the process, you can get the JVM handle
JavaVM* jvm = nullptr;
jsize vmCount = 0;
;
JNI_GetCreatedJavaVMs is part of the JNI Invocation API and returns handles to all JVMs in the current process. Once you have the JavaVM*, getting JVMTI is trivial
jvmtiEnv* jvmti = nullptr;
jvm->;
Now you have full JVMTI access without any agent setup. But there's a catch, many JVMTI capabilities (like can_retransform_classes) can only be added during the OnLoad phase, which happens before the JVM fully starts up. If you try adding them later, you'll get JVMTI_ERROR_NOT_AVAILABLE.
This is where the fake agent trick comes in.
Creating a Fake JPLISAgent
The trick is to manually construct a JPLISAgent structure that looks exactly like one the JVM would have created during proper agent loading. First, we need to replicate the structure layout from JPLISAgent.h
;
// this is the full struct from JPLISAgent.h
;
These structures must match OpenJDK's internal layout exactly, as the JVM will dereference this pointer when you use instrumentation methods. Any misalignment will cause crashes.
Now we create and initialise the fake agent
JPLISAgent*
The SetEnvironmentLocalStorage call is crucial. When JVMTI callbacks fire (like ClassFileLoadHook), the VM retrieves the agent structure using GetEnvironmentLocalStorage. If it's not set, callbacks won't work properly. This is exactly what the real initializeJPLISAgent in JPLISAgent.c does
// from JPLISAgent.c
JPLISInitializationError
We're just replicating the official setup.
There's a subtle trick with the retransform environment. By setting mRetransformEnvironment.mJVMTIEnv = nullptr but mNormalEnvironment.mJVMTIEnv = jvmti, we force the JVM to register the class file load hook in the normal environment. this is handled by the retransformableEnvironment function in JPLISAgent.c, which checks if the retransform environment already exists before creating a new one
// from JPLISAgent.c
jvmtiEnv *
By leaving our mRetransformEnvironment.mJVMTIEnv as NULL, we trick the instrumentation layer into using the normal environment for retransformation, which is what we want.
Instantiating InstrumentationImpl
Now comes the clever bit, directly instantiating sun.instrument.InstrumentationImpl via JNI.
jobject
This directly mimics the VM's own createInstrumentationImpl function in JPLISAgent.c, which is called during the VMInit phase
// from JPLISAgent.c
jboolean
As you can see, our "trick" is just a recreation of the VM's own setup process. The JVM doesn't validate this pointer at construction time, it just stores it as a field.
Adding Capabilities
There's still one problem, we need to add JVMTI capabilities, but we're past the OnLoad phase. Surprisingly, this actually works
jvmtiCapabilities caps = ;
caps.can_redefine_classes = 1;
caps.can_retransform_classes = 1;
caps.can_retransform_any_class = 1;
caps.can_set_native_method_prefix = 1;
jvmti->;
So, while the documentation says these capabilities should only be added during OnLoad, in practice OpenJDK allows them to be added later. The VM just won't retroactively affect classes which have already been loaded. But for new classes and explicitly retransformed classes, it actually works fine.
This is, again, exactly how the agent itself adds capabilities when they're requested by Instrumentation methods
// from JPLISAgent.c
void
The agent adds capabilities lazily, so we can too.
Conclusion
The Java instrumentation API seems like it requires -javaagent, but that's just the standard entry point. By understanding JPLIS internals and carefully constructing the native structures the JVM expects, we can create a fully functional Instrumentation object from scratch.
Thank you for reading <3