JVMRTDP is a diagnostics, analysis, and debugging tool for HotSpot Java Virtual Machines (JVMs) on Windows x64. It injects an agent into a target JVM and exposes class browsing, object inspection, decompilation, bytecode views, breakpoints, thread control, and Java Virtual Machine Tool Interface (JVMTI) operations through a terminal user interface (TUI), a command line, and scripts.
Use this tool only with processes you are authorized to inspect. Debugging, field writes, method calls, and class redefinition change the state of the target JVM.
- Discover and connect to local Java processes, including embedded JVMs.
- Browse runtime state by package, class, field, method, object, stack frame, and static context.
- Read and modify fields, arrays, and collections; invoke methods and constructors.
- Retain, weakly track, replace, and release named object or field references across Context changes.
- Stop on newly created Strings by content/creator-stack conditions, retain the match, and manage precise String field watches or String-bearing method entry/exit hooks.
- Decompile classes or individual methods with CFR or Procyon.
- Inspect JVM bytecode, bytecode indexes (BCI), source lines, constant-pool references, and method metadata.
- Manage breakpoints, field access/modification watchpoints, stepping, and paused threads.
- Inspect threads, frames, locals, monitors, tags, object sizes, and JVMTI properties.
- Export classes, objects, bytecode, decompiled source, and debugger snapshots.
- Integrate automation through batch files, scripts, and structured output.
- Windows x64
- Java 8 or later
- Visual Studio 2022 C++ toolchain when building native components from source
The controller, target JVM, and native agent must use compatible architectures. The current native target is x64.
.\gradlew.bat buildBuild outputs:
build/libs/JVMRTDP-2.2.0.jar
build/libs/jvmrtdp-2.2.0-library.jar
build/native-output/agent/x64/Release/jvmrtdp-agent-build.dll
JVMRTDP-2.2.0.jar is the self-contained executable. jvmrtdp-2.2.0-library.jar is the dependency-friendly library artifact and keeps terminal dependencies external.
To publish the agent DLL to the conventional location:
.\gradlew.bat publishAgentNativePublished location:
natives/x64/Release/jvmrtdp-agent.dll
The agent embedded in the JAR and a DLL preloaded by the target JVM should come from the same build. Restart the target JVM after replacing the DLL.
Start the default TUI:
java -jar build\libs\JVMRTDP-2.2.0.jarStart in command-line mode:
java -jar build\libs\JVMRTDP-2.2.0.jar --cliBasic CLI workflow:
jvmrtdp> ps
jvmrtdp> attach <pid>
target[<pid>|<unset>]> context class com.example.Application
target[<pid>|com.example.Application]> class methods
In the TUI, select a process and press Enter to connect. Long-running operations execute in the background so the interface remains responsive.
Some JVMTI capabilities are available only during JVM startup. Load the agent with -agentpath when full debugging support is required:
java -agentpath:<path-to-jvmrtdp-agent.dll> -jar application.jarPause at the main entry:
java -agentpath:<path-to-jvmrtdp-agent.dll>=break-main=com.example.Application -jar application.jarPause at a class initializer:
java -agentpath:<path-to-jvmrtdp-agent.dll>=break-clinit=com.example.Component -jar application.jarSeparate multiple startup options with commas:
java -agentpath:<path-to-jvmrtdp-agent.dll>=break-main=com.example.Application,break-clinit=com.example.Component -jar application.jarClass names may use dotted or JVM internal slash notation.
The agent can also stop before the controller attaches. Method specs use
class#method#descriptor; the descriptor is optional and may be replaced by *:
java -agentpath:<path-to-jvmrtdp-agent.dll>=break-entry=com.example.Service#run#()V -jar application.jar
java -agentpath:<path-to-jvmrtdp-agent.dll>=break-exit=com.example.Service#run#()V -jar application.jar
java -agentpath:<path-to-jvmrtdp-agent.dll>=break-exception=java.lang.*Exception -jar application.jarThe TUI browses by package and hides common JDK implementation types by default. The footer lists actions available in the current view. CLI and TUI switches preserve the same session context, decompiled content, bytecode view, debugger state, and managed breakpoints.
| Key | Action |
|---|---|
Up / Down |
Move selection |
PgUp / PgDn |
Move by page |
Home / End |
Move to the first or last item |
Tab |
Switch views |
Shift+Tab / Ctrl+Left |
Switch to the previous view |
Ctrl+Right |
Switch to the next view |
Enter |
Open the selected item |
Backspace |
Return to the parent context or package |
Left / Right |
Scroll horizontally |
[ / ] |
Scroll horizontally faster |
/ |
Filter the current list; Esc cancels |
f / F |
Find in the displayed list / search the active loaded or unloaded catalog |
: |
Open an exact class, package, field, or method target |
@ |
Show or hide static members in Fields/Methods |
# |
Show or hide instance fields and virtual methods in Fields/Methods |
= |
Set a selected field, paused local, or writable context source |
x / X |
Invoke the selected method virtually / invoke its declaring implementation |
P |
Enter a package name |
l / L |
Load and initialize a class / load and link it without running <clinit> |
J |
Show or hide JDK types |
U (Browse) |
Switch between loaded classes and the separate unloaded class-path catalog |
a |
Show or hide array types in Browse |
A |
Decompile the selected or current class |
K |
Show or hide <init> / <clinit> |
+ / - / ~ |
Insert, delete, or replace the highlighted bytecode instruction |
F2 |
Switch to the CLI |
Q |
Go back or exit |
Main views:
browse: packages and classescontext: current class, object, static value, or stack contextreferences: strong/weak object snapshots and live field slotsfields/methods: member browsing and operationsdecompile: decompiled class or method sourcebytecode: BCI, source lines, and instruction streamdebug: current stop, threads, stack, and localsstrings: conditional String allocation stops, field watches, and method entry/exit hooksframes/locals: stack frames and local variablesbreakpoints: breakpoint managementthreads: all JVM threads and states
Browse never mixes runtime classes with unloaded class files. Press U to open the
unloaded target-class-path/JDK archive catalog; rows are marked [U:C], [U:M], and [U:F].
Opening bytecode or decompilation in this mode reads the class file on the controller and
does not define or initialize it. F9 registers a symbolic pending breakpoint, u/W
register field read/write watches, and Ctrl+E/Ctrl+X register method entry/exit stops.
Press lowercase l to load and initialize the selected class, or uppercase L to load and
link it without running <clinit> so its bytecode can be redefined first.
The agent resolves these registrations during ClassPrepare, before ordinary class use.
decompile class com.example.Application --engine cfr
decompile method com.example.Application main "([Ljava/lang/String;)V" --engine procyon
decompile range com.example.Application main "([Ljava/lang/String;)V" 12 48 --engine cfr
bytecode com.example.Application main "([Ljava/lang/String;)V"
bytecode insert-before com.example.Application run "()I" 4 "LDC \"return=\" ;; INVOKESTATIC example/Trace log (Ljava/lang/String;)V"
bytecode replace com.example.Application run "()I" 4 "ICONST_5 ;; IRETURN"
bytecode flush com.example.Application
Decompile and bytecode views support search, horizontal scrolling, line or BCI navigation, breakpoints, and export. Native and abstract methods have no JVM Code attribute and therefore expose no Java bytecode.
Bytecode edits are staged before installation. In the Bytecode or Debug TUI, + inserts at the highlighted BCI (after: selects insertion after it), - deletes it, and ~ replaces it; F3 verifies and flushes the complete class transaction and Shift+F3 discards it. CLI edits use the same staging area and install only on bytecode flush. This allows temporarily incomplete operand-stack/control-flow edits to be finished before ASM recomputes frames and JVMTI verifies the class. CFR supplies direct BCI/decompiled-line mappings even without a LineNumberTable; S follows the highlighted BCI and V decompiles a selected BCI range. Managed breakpoints are relocated on flush.
Set and inspect breakpoints:
debugger enable
# The class may still be unloaded; this registration remains pending until ClassPrepare.
debugger break com.example.Application run "()V" 0
debugger watch read set com.example.Application state "I"
context class com.example.Application
debugger break-context run "()V" 0
debugger breakpoints
In the TUI, F9 always creates a normal breakpoint and Shift+F9 explicitly limits it to the object currently selected in Context. CLI/library callers can use break-context or JvmBreakpointCondition for receiver and caller conditions. TUI field watchpoints apply to every instance.
debugger break, debugger watch, and exact method entry/exit event registrations accept
unloaded class names. The controller keeps them as managed controls, and the native agent
installs their JVMTI IDs at ClassPrepare. Receiver-specific conditions still require a
live object; unloaded registrations therefore apply to every future instance.
Inspect and control threads:
debugger threads
debugger pause <thread-index>
debugger current <paused-index> 0 12
debugger locals <paused-index> 0
debugger step <paused-index>
debugger step-out <paused-index>
debugger run-to-line com.example.Application run "()V" 18 <paused-index>
debugger continue <paused-index>
The TUI frames view exposes every captured call-stack frame with its method descriptor
and BCI. Enter opens that frame in Debug, B opens Bytecode, and S decompiles the
frame. The selected frame's execution point is marked with a yellow > in Bytecode and
Decompile; press G in Frames, Bytecode, Debug, or Decompile to select and centre it.
Native frames report BCI -1 and have no Java bytecode, so select a Java caller instead.
Method events also cover methods without Java bytecode:
debugger event-break entry java.lang.Runnable run "()V" subtypes
debugger event-break exit java.lang.Object wait "()V"
debugger exception-break "java.lang.*Exception"
debugger event-breakpoints
Use debugger force-return <paused-index> <value> or force-return-void while stopped
inside a Java frame. JVMTI cannot force-return a native frame or alter a value after its
METHOD_EXIT event has already fired.
Sample a running thread:
debugger sample <thread-index> 0 12
Freeze the target for analysis:
debugger freeze
debugger snapshot debugger-snapshot.json json
debugger thaw
freeze excludes sensitive threads such as agent service threads and resumes only threads suspended by the active freeze operation.
In the TUI, F8 continues the current thread. While the target is running, the debugger view briefly suspends the tracked thread at intervals to refresh the current method, BCI, stack, and locals. Press F4 to disable live following.
Dynamic injection occurs during the JVMTI LIVE phase. At that point, the agent can add only capabilities that the JVM still reports as potential. Capabilities restricted to the OnLoad phase cannot be forced through standard JVMTI after startup.
Inspect capability state:
jvmti phase
jvmti capabilities
jvmti capability-status
jvmti capability add can_generate_method_entry_events
The command reports a failure when a capability is no longer potential. Loading the agent at startup with -agentpath is preferred over modifying HotSpot's internal capability tables.
JVMTI can read frame locations and local variables, but the standard API does not expose the current JVM operand stack. The bytecode maxStack value is only the maximum depth declared by the class file.
Tracked references and String hooks use the same managers in the CLI, TUI, and Java API:
refs save service weak
refs field currentName name strong
refs static globalName com.example.Config displayName
refs use currentName
refs set currentName string:replacement
refs release service
strings field display-write write com.example.Config displayName
strings field user-name-write write com.example.User name object
strings method parse-exit exit com.example.Parser parse (Ljava/lang/String;)Ljava/lang/String;
strings allocation token-created "*secret-token*" "com.example.*" "*" "*" fast ignore-case once
strings allocation literal-use "*license accepted*" "pack.license.*" "*" "*" fast ldc once
strings track display-write displayValue
strings use display-write
strings call display-write length ()I
Strong references keep objects alive until release. Weak references report COLLECTED after
normal GC. A tracked field reports NULL when its current value is Java null; this is distinct
from a collected weak receiver. String replacement updates the owning field/reference/local—Java
String instances remain immutable.
Allocation hooks default to fast: JVMRTDP temporarily adds lightweight probes to
String.<init> returns, so unrelated allocations and method exits do not enter a global JVMTI
callback. Content is prefiltered in the bootstrap bridge before native/JVMTI work; an all-wildcard
creator skips full stack capture.
Add the opt-in ldc policy to observe execution of matching String constants. JVMRTDP scans
already-loaded methods and places precise JVMTI breakpoints without making active frames obsolete;
filtered probes cover future class loads. Hits report returnState=ldc and pause at the method/BCI
that executed the ldc/ldc_w. This observes literal use rather than allocation, so use once,
max=N, or sample=N for code paths that execute repeatedly.
Use complete only when VM/native-created or already JIT-intrinsified String paths without an
observable constructor return must also be covered, because it enables the JVM-wide
VM_OBJECT_ALLOC event. once, max=N, and sample=N
bound stop frequency. The latest match supports read, use, call, and track, while Debug
exposes its creator stack. Use -agentpath when the required breakpoint/bytecode/local-variable
retransform/redefine capabilities (and allocation capability in complete mode) are unavailable
after dynamic attach. The probe is removed when the last allocation hook is removed.
Batch mode executes target commands directly:
batch commands.txt
The script language adds variables, labels, conditional branches, object references, and export:
script workflow.jrd
See the Scripting Guide for the full syntax. Use debugger snapshots in json or jsonl format when another program consumes debugger data.
Publish the current build to the local Maven repository:
.\gradlew.bat publishToMavenLocalrepositories { mavenLocal() }
dependencies { implementation("nhcm.jvmrtdp:jvmrtdp:2.2.0") }Discover and attach to a JVM from Java:
import nhcm.jvmrtdp.api.JvmRtdpClient;
import nhcm.jvmrtdp.api.JvmRtdpCommandResult;
import nhcm.jvmrtdp.api.JvmRtdpSession;
try (JvmRtdpClient client = JvmRtdpClient.open();
JvmRtdpSession session = client.attach(pid)) {
JvmRtdpCommandResult result = session.execute("jvmti phase").requireSuccess();
System.out.print(result.standardOutput());
}The library API provides process discovery, configurable attach, captured command results,
asynchronous agent commands, direct JNI/JVMTI access, session.references(),
session.stringHooks(), and session.instrumentation() for source/JAR deployment, callbacks,
transformers, transactional ASM bytecode patches, retransformation, and redefinition. See the
Java Library Guide.
- The agent listens only on the loopback interface and generates a random token for each session.
- Debugging, thread suspension, method calls, field writes, GC, and class redefinition affect the target application.
- Object handles are generally strong references; release handles that are no longer needed during long sessions.
- HotSpot schema restrictions limit class redefinition; fields and methods cannot be added or removed arbitrarily.
- Invoking a blocking method remotely blocks that request until the method returns.
- Validate required capabilities and operations in an equivalent test environment before production use.