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.

published: reading time: 22 min read author: GeekWorkBench

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 -agentlib or -agentpath
  • Agent_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.

CapabilityWhat it allows
can_tag_objectsTag objects for later identification during heap walks
can_generate_field_modification_eventsBreak when a specific field is modified
can_generate_single_step_eventsEvent on every bytecode instruction
can_get_constant_poolRead the class constant pool
can_suspendSuspend and resume threads
can_access_local_variablesRead/write local variables at any bytecode
can_generate_exception_eventsBreak when exceptions are thrown
can_redefine_classesModify 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

AspectJVMTI AgentJava Agent (JVMTI via JNI)JFR
LanguageC/C++ onlyJava + native bridgeBuilt-in
Startup-agentpath at JVM start-javaagent-XX:StartFlightRecorder
AttachAgent_OnAttachUnsupported (Attach API limited)Yes (jcmd)
Overhead (full)5-20%3-15%1-5%
Overhead (sampling)1-3%1-3%1-3%
Heap accessFullVia JNIPartial
Custom eventsYesYesLimited
ComplexityVery highHighLow

Observability Checklist

  • Use async-profiler for production CPU sampling instead of METHOD_ENTRY events
  • Request only the capabilities you need; each capability can add overhead
  • Never perform blocking operations (I/O, locks) inside event callbacks
  • Set up Agent_OnUnload to clean up resources
  • UseJVMTI’s GenerateEvents to force events you need for analysis
  • Understand the difference between can_generate_single_step_events (per bytecode) vs. sampling
  • Use SetEventNotificationMode to disable events when not needed
  • Implement graceful degradation if an event queue fills up
  • Test agent with -XX:+PrintFlagsFinal to 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 -agentpath or dynamically via Attach API
  • Request only needed capabilities; each adds overhead
  • METHOD_ENTRY/EXIT events are too expensive for production
  • Never block inside callbacks; use a handler thread
  • Use JVMTI Allocate/Deallocate for native memory, not malloc
  • 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

1. What is JVMTI and how does it differ from JMX and JFR?

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.

2. Why are METHOD_ENTRY and METHOD_EXIT events generally unsuitable for production 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%.

3. How do you safely handle memory in a JVMTI agent?

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.

4. What are the security implications of JVMTI agents in production?

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.

5. What are the specific event callbacks available in JVMTI and which are safe for production?

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.

6. How do you compile and debug a JVMTI agent on Linux?

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.

7. How does the Attach API enable dynamic agent loading without JVM restart?

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.

8. What is the difference between IterateOverHeap, IterateOverChunkedHeap, and IterateOverReachableHeap?

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.

9. How does JVMTI handle the distinction between tagged and untagged objects?

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.

10. What is the purpose of jvmtiCapabilities and which ones require extra caution?

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.

11. How does JVMTI's StackTrace function work and what are its limitations?

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.

12. What is the difference between jvmtiHeapReferenceInfo types in heap reference callbacks?

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.

13. How do you prevent memory leaks inside a JVMTI agent itself?

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.

14. What is the JvmtiExport table and how does it relate to callbacks?

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.

15. How does async-profiler's frame pointer approach differ from JVMTI stack walking?

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.

16. What is the can_redefine_classes capability and what are its practical uses?

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.

17. How does JVMTI interact with JVMCI (JVM Compiler Interface) and AOT-compiled code?

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.

18. What are the specific memory barriers or synchronization points in JVMTI event delivery?

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.

19. How do you use JVMTI to detect and analyze memory leaks in a production JVM?

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.

20. What is the difference between JVMTI callbacks and polling approaches for event collection?

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

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.

#jvm #crash #sigsegv

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.

#jvm #jit #deoptimization

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.

#jvm #java #profiling