JVMTI Agents: Profiling and Debugging with the JVM Tool Interface
Explore the JVM Tool Interface for building profiling, debugging, and monitoring agents that hook deep into the JVM runtime.
JVMTI Agents: Profiling and Debugging with the JVM Tool Interface
The Java Virtual Machine Tool Interface (JVMTI) is a native API that provides the foundation for virtually every professional Java diagnostics and profiling tool. JVMTI gives you direct access to JVM runtime events: thread lifecycle, class loading, method entry/exit, field access, garbage collection, object allocation, and more. You implement JVMTI as a shared library (Agent) that the JVM loads at startup or attaches dynamically.
This guide covers how JVMTI works architecturally, how agents are structured, which capabilities you can request, and the mistakes that bite most developers their first time writing an agent.
Introduction
The Java Virtual Machine Tool Interface (JVMTI) is a native C/C++ API that provides the deepest level of access to the JVM runtime. Every professional Java profiling, debugging, and diagnostics tool relies on JVMTI under the hood — JProfiler, YourKit, async-profiler, Java Flight Recorder, and even the JVM’s own JMX implementation all connect through this interface. JVMTI lets you subscribe to callbacks for events like method entry and exit, garbage collection start and finish, object allocation, thread lifecycle changes, and breakpoints. It also lets you query and modify JVM state from native code: walking the heap graph, reading local variables, forcing garbage collection, and redefining class bytecode at runtime.
JVMTI matters to practitioners who build diagnostics tools, need to understand why a profiling agent is behaving a certain way, or are debugging subtle agent integration issues. The most common pain point is performance overhead: METHOD_ENTRY and METHOD_EXIT callbacks fire on every method invocation and add 5-20% overhead, which makes them unusable in production. Understanding which capabilities add overhead, why callbacks must never block, and how memory management differs from standard C library functions prevents the mistakes that bite most developers writing their first JVMTI agent. For production profiling, knowing that async-profiler uses JVMTI plus OS signals rather than expensive event callbacks explains why it achieves sub-2% overhead while still providing accurate call stacks.
This guide covers JVMTI architecture, the structure of native agents with Agent_OnLoad and Agent_OnAttach entry points, which capabilities are safe for production versus which add prohibitive overhead, building a simple allocation tracking agent, and the security implications of loading native code into the JVM process. By the end, you will understand how diagnostics tools integrate with the JVM, what trade-offs each capability choice imposes, and how to avoid the memory management, thread safety, and build architecture mistakes that cause agent failures.
What is JVMTI?
JVMTI is a C/C++ interface defined in the JVM specification. It is not a Java API. It is the door through which native code inspects and influences the JVM at runtime. Tools like JProfiler, YourKit, async-profiler, and even the JVM’s own JMX implementation all use JVMTI under the hood.
JVMTI replaces the old JVMPI (Java Virtual Machine Profiler Interface) and JVMDI (Java Virtual Machine Debug Interface), both of which were deprecated.
When a JVMTI agent attaches, it receives callbacks for events it has subscribed to, and it can call JVMTI functions to query or modify JVM state.
When to Use JVMTI
Ideal Use Cases
- CPU profiling: Sampling call stacks with very low overhead (async-profiler uses JVMTI + SIGPROF signal)
- Heap memory analysis: Walking the heap graph, counting objects, finding leaks (MAT, VisualVM)
- Debugging: Setting breakpoints, stepping through code, inspecting variables
- Coverage tools: Tracking which code paths executed (JaCoCo uses Java agents, but JVM TI could)
- Custom monitoring: Building specialized diagnostics beyond what JMX or JFR offer
When NOT to Use JVMTI
- Simple metrics: JMX or JFR handle common cases with far less complexity
- Non-native environments: Writing C/C++ agents is significantly harder than Java
- Production with high overhead tolerance: JFR is already built-in and has lower overhead than most custom agents
- Quick prototyping: A Java agent using JVMTI through JNI is more practical than writing native C/C++
Architecture
graph TB
subgraph "JVM Process"
JVM[JVM Runtime]
subgraph "JVMTI"
Env[JVMTI Environment]
CBF[Callback Functions]
CAP[Capabilities]
end
subgraph "Native Agent"
Agent[Agent Library .so/.dll]
JNI[JNI Bridge]
end
end
subgraph "Agent Lifecycle"
Init[Agent_OnLoad<br/>or Agent_OnAttach]
EVT[Event Subscriptions]
Loop[Event Loop<br/>Callbacks]
end
Init --> EVT
EVT --> Loop
JVM -->|JVMTI Events| CBF
CBF -->|JVMTI Functions| Env
Env -->|Native Code| JNI
JNI -->|Java Classes| JVM
Agent Entry Points
JVMTI agents have two possible entry points:
Agent_OnLoad: Called when the JVM starts with the agent attached via-agentlibor-agentpathAgent_OnAttach: Called when dynamically attaching to a running JVM via the Attach API
// agent.c
#include <jvmti.h>
static jvmtiEnv *jvmti = NULL;
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {
jint result = (*jvm)->GetEnv(jvm, (void **) &jvmti, JVMTI_VERSION_1_2);
if (result != JNI_OK) {
fprintf(stderr, "ERROR: GetEnv failed\n");
return JNI_ERR;
}
// Set capabilities
jvmtiCapabilities caps;
memset(&caps, 0, sizeof(caps));
caps.can_tag_objects = 1;
caps.can_get_constant_pool = 1;
caps.can_generate_single_step_events = 1;
(*jvmti)->AddCapabilities(jvmti, &caps);
// Set callbacks for events
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.MethodEntry = on_method_entry;
callbacks.MethodExit = on_method_exit;
callbacks.GarbageCollectionStart = on_gc_start;
callbacks.GarbageCollectionFinish = on_gc_finish;
(*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));
// Enable events
(*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
JVMTI_EVENT_METHOD_ENTRY, NULL);
(*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
JVMTI_EVENT_GARBAGE_COLLECTION_START, NULL);
return JNI_OK;
}
Key Capabilities
JVMTI separates available functionality into capabilities that an agent must explicitly request. The JVM grants capabilities that are available on your JVM implementation.
| Capability | What it allows |
|---|---|
can_tag_objects | Tag objects for later identification during heap walks |
can_generate_field_modification_events | Break when a specific field is modified |
can_generate_single_step_events | Event on every bytecode instruction |
can_get_constant_pool | Read the class constant pool |
can_suspend | Suspend and resume threads |
can_access_local_variables | Read/write local variables at any bytecode |
can_generate_exception_events | Break when exceptions are thrown |
can_redefine_classes | Modify class bytecode at runtime |
Implementation: Building a Simple Allocation Tracker
This agent tracks large object allocations by hooking into the ObjectFree event and sampling allocation sites:
// alloc_tracker.c
#include <jvmti.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static jvmtiEnv *jvmti = NULL;
static FILE *output = NULL;
static jvmtiEnv *all_envs[10];
void JNICALL
ObjectFree(jvmtiEnv *jvmti_env, jlong tag) {
if (tag == 0) return;
// Object freed - log if it was large
fprintf(output, "FREE: tag=%lld\n", tag);
}
void JNICALL
VMObjectAlloc(jvmtiEnv *jvmti_env, JNIEnv *jni,
jthread thread, jobject object,
jclass object_klass, jlong size) {
if (size > 1024 * 1024) { // > 1MB
jclass klass = object_klass;
char *signature = NULL;
(*jvmti_env)->GetClassSignature(jvmti_env, klass, &signature, NULL);
fprintf(output, "LARGE_ALLOC: size=%lld class=%s thread=%p\n",
size, signature ? signature : "unknown", thread);
if (signature) (*jvmti_env)->Deallocate(jvmti_env, (unsigned char*)signature);
}
}
jint JNICALL
Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {
jint result = (*jvm)->GetEnv(jvm, (void **)&jvmti, JVMTI_VERSION_1_2);
if (result != JNI_OK) return JNI_ERR;
output = fopen("/tmp/alloc_tracker.log", "w");
jvmtiCapabilities caps = {0};
caps.can_generate_vm_object_alloc_events = 1;
caps.can_tag_objects = 1;
(*jvmti)->AddCapabilities(jvmti, &caps);
jvmtiEventCallbacks callbacks = {0};
callbacks.VMObjectAlloc = VMObjectAlloc;
callbacks.ObjectFree = ObjectFree;
(*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));
(*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
JVMTI_EVENT_VM_OBJECT_ALLOC, NULL);
(*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
JVMTI_EVENT_OBJECT_FREE, NULL);
return JNI_OK;
}
void JNICALL
Agent_OnUnload(JavaVM *jvm) {
if (output) fclose(output);
}
Compile and use:
# Compile
gcc -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux \
-o liballoc_tracker.so alloc_tracker.c
# Run with agent
java -agentpath:./liballoc_tracker.so -jar myapp.jar
Production Failure Scenarios
Scenario 1: Agent Overhead Causing Performance Degradation
Symptom: Adding an agent drops throughput by 40% and doubles CPU usage.
Investigation: Agent was subscribing to METHOD_ENTRY and METHOD_EXIT on all threads. Every bytecode instruction execution triggered two JNI callbacks. This is the most expensive possible profiling mode.
Solution: Switch to sampling-based profiling via SIGPROF (async-profiler approach) or limit event subscription to specific classes.
Lesson: METHOD_ENTRY/METHOD_EXIT events are almost never acceptable in production. Use allocation sampling or CPU sampling via signals instead.
Scenario 2: JVMTI Heap Walk Triggering OOM
Symptom: Agent calls IterateOverHeap and JVM crashes with OOM during the walk.
Investigation: Walking the heap requires the JVM to hold GC root references while traversing. On large heaps (100GB+), this causes temporary allocation spikes that trigger OOM.
Solution: Use chunked iteration (IterateOverChunkedHeap) and do not hold references across callback invocations.
Scenario 3: Event Callback Blocking Causing Deadlocks
Symptom: Application deadlocks when GC runs while agent holds a lock in its callback.
Investigation: The GC GarbageCollectionFinish callback tried to write to a file while the agent initialization had already opened that file descriptor from another thread.
Solution: Never do blocking I/O inside JVMTI callbacks. Queue events to a background thread.
Trade-off Table
| Aspect | JVMTI Agent | Java Agent (JVMTI via JNI) | JFR |
|---|---|---|---|
| Language | C/C++ only | Java + native bridge | Built-in |
| Startup | -agentpath at JVM start | -javaagent | -XX:StartFlightRecorder |
| Attach | Agent_OnAttach | Unsupported (Attach API limited) | Yes (jcmd) |
| Overhead (full) | 5-20% | 3-15% | 1-5% |
| Overhead (sampling) | 1-3% | 1-3% | 1-3% |
| Heap access | Full | Via JNI | Partial |
| Custom events | Yes | Yes | Limited |
| Complexity | Very high | High | Low |
Observability Checklist
- Use async-profiler for production CPU sampling instead of
METHOD_ENTRYevents - Request only the capabilities you need; each capability can add overhead
- Never perform blocking operations (I/O, locks) inside event callbacks
- Set up
Agent_OnUnloadto clean up resources - UseJVMTI’s
GenerateEventsto force events you need for analysis - Understand the difference between
can_generate_single_step_events(per bytecode) vs. sampling - Use
SetEventNotificationModeto disable events when not needed - Implement graceful degradation if an event queue fills up
- Test agent with
-XX:+PrintFlagsFinalto understand JVM configuration impact
Security Notes
JVMTI agents run with full native code privileges inside the JVM process:
- A malicious agent can execute arbitrary code, read memory, or crash the JVM
- Agents can bypass Java security checks entirely
- Attach API allows loading agents into running JVMs if the process permits
Never load untrusted JVMTI agents. In production:
- Restrict access to agent loading mechanisms
- Sign agent libraries and verify signatures before loading
- Use JVM flags to disable agent attach if not needed:
-XX:+DisableAttachMechanism - Audit which agents are loaded in your deployment pipeline
Common Pitfalls / Anti-Patterns
Pitfall 1: Forgetting to Check Capability Availability
Problem: Agent fails on a different JVM because a capability is not supported.
Solution: Call GetCapabilities after AddCapabilities to verify what was actually granted. Handle gracefully if a needed capability is missing.
Pitfall 2: Memory Management Mistakes
Problem: JVMTI has its own allocation (Allocate) and deallocation (Deallocate) functions. Mixing with malloc/free causes crashes.
Solution: Always use JVMTI’s Allocate for memory that will be freed by JVMTI. Use JNI NewGlobalRef/DeleteGlobalRef for Java object references.
Pitfall 3: Thread Safety Issues in Callbacks
Problem: Multiple JVM threads can fire callbacks simultaneously. Your callback code must be thread-safe.
Solution: Protect shared state with locks, or better yet, avoid shared mutable state in callbacks entirely by using lock-free queues to pass data to a dedicated handler thread.
Pitfall 4: Releasing Object References Incorrectly
Problem: JNI local references not deleted in callbacks cause local reference table overflow.
Solution: Use PushLocalFrame/PopLocalFrame in deep JNI call chains within callbacks, or explicitly delete local refs with DeleteLocalRef.
Pitfall 5: Not Building for the Right Architecture
Problem: Agent compiled for x86 crashes on ARM64 JVM or vice versa.
Solution: Know your target JVM architecture. Cross-compile or build separate agents per architecture. Check with java -version output that includes architecture.
Quick Recap Checklist
- JVMTI is a native C/C++ API for deep JVM inspection
- Agents attach at startup via
-agentpathor dynamically via Attach API - Request only needed capabilities; each adds overhead
-
METHOD_ENTRY/EXITevents are too expensive for production - Never block inside callbacks; use a handler thread
- Use JVMTI
Allocate/Deallocatefor native memory, notmalloc - Handle thread safety in callbacks or face race conditions
- async-profiler uses JVMTI for safe production profiling
- Lock down agent loading in production; untrusted agents are a security risk
- Clean up resources in
Agent_OnUnload
Interview Questions
JVMTI (Java Virtual Machine Tool Interface) is a native C/C++ API that provides the deepest level of JVM access, including bytecode-level events, heap walking, and thread control. JMX and JFR are Java-level interfaces built on top of JVMTI. JMX exposes managed beans through a standardized management API, while JFR is a built-in event recording system. JVMTI gives you raw access to things that neither JMX nor JFR can reach, but it requires writing native code. For most production diagnostics, JFR is already sufficient and far easier to use.
Every METHOD_ENTRY and METHOD_EXIT callback fires for every single method invocation, which means two callbacks per method call across your entire application. At scale, this generates millions of callbacks per second and adds 5-20% overhead, sometimes much more. The callbacks also execute inside the JVM's execution path, adding latency to every method call. For production profiling, sampling-based approaches like async-profiler's SIGPROF signal handling are vastly superior because they capture call stacks at controlled intervals with overhead typically under 2%.
JVMTI has its own memory management functions that you must use instead of standard C library functions. Use Allocate to allocate native memory and Deallocate to free it. Never mix malloc/free with JVMTI allocations. For Java object references, use JNI NewGlobalRef to keep a Java object alive from native code and DeleteGlobalRef to release it. Inside callbacks that make JNI calls, use PushLocalFrame/PopLocalFrame to manage local references, or explicitly call DeleteLocalRef to avoid overflowing the local reference table.
A JVMTI agent runs as native code inside the JVM process with full privileges. It can read and write arbitrary memory, bypass Java security checks, execute system calls, and crash the JVM. This means a compromised or malicious agent is equivalent to a full compromise of the JVM process. Never load agents from untrusted sources. Disable the Attach API in production with -XX:+DisableAttachMechanism if agents are not needed. Sign agent libraries and verify signatures before loading. Restrict access to any mechanism that can load agents.
JVMTI provides dozens of event callbacks including: MethodEntry/MethodExit (too expensive for production — fires on every method invocation), SingleStep (per-bytecode, extremely expensive), Breakpoint (safe when limited to specific locations), FieldAccess/FieldModification (moderate overhead), Exception/ExceptionCatch (low overhead), ThreadStart/ThreadEnd (low), ClassLoad/ClassPrepare (low-medium), GarbageCollectionStart/GarbageCollectionFinish (low), ObjectFree (low), and VMObjectAlloc (low-medium with sampling threshold). Only VMObjectAlloc with sampling, ObjectFree, and Exception events are generally considered safe for production use with proper configuration.
Compile with: g++ -shared -fPIC -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux -o libagent.so agent.cpp. For debugging, compile with -g -O0 and use LD_LIBRARY_PATH=./build:$LD_LIBRARY_PATH java -agentpath:./build/libagent.so. Common issues: wrong architecture (compile for x86_64 on x86_64 JVM, aarch64 on ARM), missing JNIEXPORT annotations causing link errors, forgetting to link against libjvm.so when needed, and mixing stdlib implementations. Use file libagent.so and readelf -d libagent.so | grep NEEDED to verify the binary.
The Attach API (com.sun.tools.attach) lets you dynamically load an agent into a running JVM via VirtualMachine.attach(pid) followed by loadAgent(agentPath, agentArgs). This calls the agent's Agent_OnAttach function (not Agent_OnLoad) in the target JVM. The Attach API uses a local socket connection to communicate with the target JVM's attach mechanism. Requirements: the target JVM must have -XX:+EnableAttachMechanism (default on), and you must have the same JVM binaries as the target process. Security: disable with -XX:+DisableAttachMechanism in production since it allows code injection.
IterateOverHeap walks all objects in the heap synchronously — it holds GC roots and traverses the entire heap graph in one call. On large heaps this causes long pauses and memory spikes. IterateOverChunkedHeap walks the heap in chunks, yielding periodically to allow GC to run between chunks — better for large heaps but still stop-the-world. IterateOverReachableHeap only walks objects reachable from GC roots (live objects), ignoring unreachable objects waiting for finalization. For leak analysis, use IterateOverReachableHeap with a heap reference callback to build a retention graph.
Object tagging is a way to mark specific objects with 64-bit tags for identification during subsequent heap walks. Use SetTag to tag an object and GetTag to retrieve it. Tags are persisting only within a single JVM session — they are not serialized in heap dumps. Tags are useful for: tracking large allocations by tagging them during VMObjectAlloc callbacks, identifying specific objects of interest during heap analysis, and correlating allocation sites with heap retention. Tags are stored in a JVM-internal side table, not in the objects themselves, so they do not modify object layout.
Capabilities are optional features the agent requests before using. Requesting a capability does not automatically enable its events — you still need to subscribe to events. Some capabilities have significant performance implications: can_generate_all_compile_method_enter_events is equivalent to METHOD_ENTRY and very expensive; can_generate_single_step_events fires on every bytecode and is extremely expensive; can_access_local_variables is safe; can_tag_objects is safe but uses a side table. Always check GetCapabilities after AddCapabilities to see what was actually granted, and only request what you need.
GetStackTrace returns the stack frames for a given thread. It works by walking the thread's stack using the JVM's internal stack walking infrastructure, which respects the JVM's safepoint semantics — it can only get reliable traces when the thread is at a safepoint or near a safepoint. You cannot get a reliable stack trace for a thread running in arbitrary native code without a safepoint. The returned frames include compiled Java methods, interpreted frames, JNI native frames, and JVM internal frames. For async-profiler, this is why SIGPROF signals are used — they interrupt at safepoints where stack walking is safe.
Heap reference callbacks receive a jvmtiHeapReferenceInfo struct whose fields vary by reference kind. For JVMTI_HEAP_REFERENCE_FIELD, the info contains the field index and declaring class. For JVMTI_HEAP_REFERENCE_ARRAY_ELEMENT, it contains the array index. For JVMTI_HEAP_REFERENCE_JNI_LOCAL, it contains thread and depth info. For JVMTI_HEAP_REFERENCE_STACK_LOCAL, it contains the slot and mutex. The reference kind determines which fields in the union are valid. Understanding these helps you build accurate retention chains in heap analysis tools.
Agent memory leaks are especially dangerous because they are inside the JVM process and can affect JVM stability. Common causes: leaking JVMTI Allocate buffers, accumulating untagged global references (JNI NewGlobalRef without DeleteGlobalRef), and storing callbacks in global state without cleanup. Prevention: always pair Allocate with Deallocate, use RAII patterns in C++, carefully audit every NewGlobalRef and DeleteGlobalRef, use Agent_OnUnload to clean up all resources, and periodically run the agent with valgrind or AddressSanitizer to detect leaks during development.
The JvmtiExport table is an internal JVM mechanism that allowsJVMTI agents to receive callbacks for events that occur at specific points in the JVM lifecycle, even before the agent has fully initialized or after it has started unloading. It provides a way for agents to subscribe to "early" events (before Agent_OnLoad completes) or "late" events (during JVM shutdown). Most agents do not need to interact with this directly — the standard callback mechanism via SetEventCallbacks handles it. But understanding JvmtiExport is important when debugging why an agent's callback is not firing at the expected time.
async-profiler uses OS signals (SIGPROF) to interrupt the JVM and then walks the stack using frame pointers rather than the JVM's internal stack walking. This is significantly faster because it does not require the thread to be at a safepoint — the OS interrupt naturally stops the thread at any instruction. async-profiler uses DWARF debug info and frame pointers to reconstruct the call stack. The tradeoff is that in some configurations (e.g., with callee-saved registers), frame pointer-based walking can miss frames that JVM safepoint-based walking would capture, but in practice it is highly accurate for CPU profiling on x86 and ARM.
can_redefine_classes allows an agent to modify class bytecode at runtime using RedefineClasses. This is used by tools like JRebel and HotSwapAgent for hot code replacement without restarting the JVM. The redefinition replaces the constant pool and method bytecode but preserves object instances where possible. Limitations: you cannot add new fields or methods, you cannot change the schema of existing fields, and changes to one class can trigger deoptimization of dependent code. The practical benefit is faster development iteration — you can update business logic without a full redeploy.
JVMCI allows a Java-based compiler (like Graal) to be used as the JIT compiler for the JVM. When JVMCI-compiled code is running, JVMTI events still fire normally — the compiler interface is below JVMTI in the stack. However, when inspecting AOT (ahead-of-time compiled) code via heap walks or stack traces, you may see methods with no debug info since AOT compilation typically does not preserve full symbol tables. Use GetSourceDebugExtension to access debug info when available, and be aware that AOT code may not appear in all profiling events the same way JIT code does.
JVMTI event callbacks are delivered at specific JVM safe points — moments when all threads are at a safe point and the heap is consistent. This means you cannot receive events for code running in a non-safe native region, and you cannot safely allocate JVM heap memory inside a callback (because the GC might be running). Callbacks execute while the JVM's internal locks may be held, so callbacks must not call blocking operations, acquire locks that other threads might hold, or trigger JNI calls that could cause deadlocks. The safe point guarantee is what makes heap walking possible but also what limits when and how events are delivered.
For production leak detection via JVMTI, use a three-phase approach: First, use ObjectFree callbacks to track when large objects are freed and compare with allocation sites. Second, periodically take chunked heap snapshots using IterateOverReachableHeap and compare object counts by class across snapshots — objects that grow in count without corresponding frees indicate a leak. Third, use object tagging during allocation tracking to mark suspected leak objects, then do a targeted retention walk to find the GC root chain keeping them alive. For production safety, limit heap walks to off-peak hours and use chunked iteration to avoid long pauses.
JVMTI callbacks fire synchronously when events occur — the JVM calls your agent function immediately when, say, a method enters or an allocation happens. This gives immediate notification with low latency. Polling approaches (like periodically calling GetThreadListStackTrace or checking GetStackTrace) sample state at discrete intervals and may miss short-duration events entirely. The trade-off is callbacks can add overhead if the event fires very frequently, while polling misses ephemeral events. For production profiling of rare events (Exceptions, wait times, allocation above threshold), callbacks are efficient enough. For high-frequency events (every method entry/exit), sampling via signals is the better approach.
Further Reading
- async-profiler - Production-safe CPU and allocation profiling using JVMTI + OS signals
- JVM TI C Unit Tests - Official JVM TI test suite for reference
- JNA Library - Simplified JNI for native interop in JVMTI agents
- JVMTI vs Java Agents - Comparison of native vs Java-based agent approaches
Conclusion
JVMTI agents provide the deepest level of JVM access for building profiling, debugging, and monitoring tools. They require native C/C++ code and careful memory management using JVMTI’s Allocate/Deallocate functions. For production profiling, prefer async-profiler which uses JVMTI safely via OS signals rather than expensive METHOD_ENTRY/METHOD_EXIT callbacks. Always restrict agent loading in production and never load untrusted agents.
Category
Related Posts
Crash Dump Analysis: HsErr Files, Core Dumps, SIGSEGV
Learn how to analyze JVM crash dumps, interpret HsErr files, extract meaningful data from core dumps, and debug native SIGSEGV errors in Java applications.
Deoptimization Debugging: When JIT Compiled Code Reverts
Learn what causes the JVM to deoptimize JIT-compiled code, how to detect deoptimization events, and how to fix the underlying issues.
Java Flight Recorder: Continuous Monitoring and Diagnostics
Learn how Java Flight Recorder captures low-level diagnostics, profiling data, and continuous monitoring events from the JVM in production environments.