With this post we’ll explore how Java / Scala debuggers we use everyday are written and work. We'll cover the two key components in the JVM Debugging Architecture - the Java Debugger Wire Protocol(JDWP) and the JVM Tooling Interface (JVMTI) C++ API. Each comes with its own set of capabilities and disadvantages.
How To Write Your Own Java
and Scala Debugger
The JVM Debugging Architecture
This JVM debugging framework and its APIs are completely open, documented and extensible, which means
you can write you own debugger fairly easily.
The framework’s current design is built out of two main parts –
1. The JDWP protocol
2. The JVMTI API layer
Each has its own set of benefits and use-cases for which it works best.
The Java Debugger Wire Protocol
JWDP is used to pass requests and receive events (such as changes in thread states or exceptions) between
the debugger and debuggee process.
This is done using binary messages, usually over the network.
The concept behind this architecture is to create as much separation as possible between the two.
The goal is to reduce the Heisenberg effect (Werner that is, not Walt) of having the debugger alter the
execution of the target code while it’s running.
The Java Debugger Wire Protocol (2)
Removing debugger logic from the target process also helps make sure that changes in the debuggee state
(such as “stop the world” GC, or OutOfMemoryError) do not affect the debugger.
The JDK comes with the JDI (Java Debugger Interface) which provides a complete debugger side
implementation of the protocol, with the ability to connect, monitor and manipulate the target VM.
This protocol is the same one used by Eclipse’s debugger for example.
If you look at the command line arguments passed to your java process when it’s debugged by the IDE you’ll
notice the arguments (-agentlib:jdwp=transport=dt_socket,…) passed to it to enable JDWP debugging.
The JVM Tooling Interface (JVMTI)
The 2nd key component in the JVM debugger architecture is a set of native APIs covering a wide range of
areas relating to the JVM.
The JVMTI is designed as a set of C++ APIs along with a JVM mechanism to load libraries (such as a .dll or .so)
that use them.
This approach differs from JDWP, as it executes the debugger inside the target process.
The downside is increased possibility of the debugger impacting the code performance and stability.
The key advantage however is the ability to interact directly with the JVM in near real-time.
Writing Your Debugger Library
Writing your own debugger requires creating a native OS library in C++.
The headers are available in jvmti.h which comes with the JDK.
Your “main” function in this case would look like -
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void*)
The function will be invoked by the JVM when your debugger agent is loaded by the JVM.
The JavaVM pointer passed to you will provide you with everything you need to converse with the JVM.
The jvmtiEnv class introduced through the GetEnv method enables interaction with JVMTI through the
concept of capabilities and events.
One of the key aspects of writing a debugger is to be mindful of its effects on the target process.
To help you get fine grained control over how you affect execution of code, the JVMTI specification introduces a
concept of capabilities.
Using this approach you can tell the JVM in advance which API commands you intend to use (i.e. set breakpoints,
This enables the JVM to prepare in advance, and gives you more control over your run-time overhead.
This also enables JVMs from different vendors to tell you which commands they support out of the entire JVMTI
Not All Capabilities Are Created Equal
Some JVMTI capabilities come at a relatively small performance overhead.
Other ones, such as can_generate_exception_events for exception callbacks,
or can_generate_monitor_events for object locking come at a higher cost.
The reason is they prevent the JVM from optimizing the code during JIT compile to its full extent, and can
force the JVM to drop into interpreted mode at run-time.
Others such as can_generate_field_modification_events for setting watches come at an even higher cost -
slowing code execution by a significant percentage.
While HotSpot supports multiple debugger libraries concurrently, some capabilities such as can_suspend for
suspending threads can only owned by one library at a time.
Setting JVMTI Callbacks
Once you’ve received your set of capabilities, your next step is to set up callbacks to let you know when
things actually happen.
Each of those callbacks provides fairly deep information as to the event that has transpired.
For an exception callback this includes the bytecode location in which the exception was thrown, the thread,
the exception object and if and where it will be caught.
void JNICALL ExceptionCallback(jvmtiEnv *jvmti,
JNIEnv *jni, jthread thread, jmethodID method,
jlocation location, jobject exception,
jmethodID catch_method, jlocation catch_location)
Setting Breakpoints and Watches
The SetBreakpoint method enables you to suspend execution at a specific byte code instruction.
SetFieldModificationWatch lets you pause execution whenever a field is modified.
At that point you can use other functions such as GetStackTrace and GetThreadInfo to learn more
about your current position in the code.
Most JVMTI functions refer to classes and methods using abstract handles such as a jmethodID and
jclass (same ones used by the Java Native Interface API).
Additional functions such as GetMethodName and GetClassSignature help you obtain the actual
Attaching Your Debugger
Once you’ve written your debugger library the next step is to attach it to a JVM.
To connect via JDWP add the following startup argument to debugee process -
This details the form of communication (ie.e sockets) to the target and whether to start the debuggee in
To attach a JVMTI library pass an agentpath argument to the debuggee process pointing to your library’s
location on disk.
An alternative way is to append your arguments to the global JAVA_TOOL_OPTIONS environment variable.
This var gets picked up by every new JVM and automatically appended to the list of its existing arguments.
Another method to attach your debugger is by using the remote attach API.
This simple and powerful Java API enables you to attach agents to running JVM processes without them being
launched with any command line arguments.
The downside is that you will not have access to some of the capabilities (such as
can_generate_exception_events) as these can only requested at VM startup.