Mobile Development 25 min read

Unlock Android JVMTI: Bypass Debug Limits and Build Powerful Profilers

This article explains how Android's JVMTI (ART Tooling Interface) works, details its monitoring capabilities, shows how to overcome debug‑only restrictions by modifying JDWP and Runtime flags, and describes the TBProfiler tool that leverages JVMTI for method, thread, exception, lock, and memory profiling in production apps.

Alibaba Terminal Technology
Alibaba Terminal Technology
Alibaba Terminal Technology
Unlock Android JVMTI: Bypass Debug Limits and Build Powerful Profilers

Android JVMTI

JVMTI (JVM Tool Interface) is a native programming interface provided by the Java Virtual Machine that allows developers to debug, monitor, and even modify Java programs at runtime.

Android supports JVMTI starting from Android 8.0 (API 26), where it is officially called ART Tooling Interface (ART TI). Important features include runtime state monitoring, class redefinition, object allocation and GC tracking, heap object traversal, stack inspection, thread suspension/resumption, and more.

To use JVMTI you must provide an Agent that communicates with ART TI and the Runtime. The Agent can be loaded at JVM startup or at runtime; on Android it must be loaded at runtime via the am command or the Debug interface.

dalvikvm -Xplugin:libopenjdkjvmti.so -agentpath:/path/to/agent/libagent.so …

VM startup loading is not suitable for Android apps because they are forked from a running zygote process, so only runtime loading is possible.

adb shell 'am attach-agent com.example.android.displayingbitmaps\data\data\com.example.android.displayingbitmaps\code_cache\libfieldnulls.so=Ljava/lang/Class;.name:Ljava/lang/String;'

The Debug interface loads the agent via attachJvmtiAgent, which is available from Android 9.0.

/**
 * Attach a library as a jvmti agent to the current runtime, with the given classloader
 * determining the library search path.
 * Note: agents may only be attached to debuggable apps. Otherwise, this function will
 * throw a SecurityException.
 *
 * @param library the library containing the agent.
 * @param options the options passed to the agent.
 * @param classLoader the classloader determining the library search path.
 * @throws IOException if the agent could not be attached.
 * @throws SecurityException if the app is not debuggable.
 */
public static void attachJvmtiAgent(@NonNull String library, @Nullable String options,
        @Nullable ClassLoader classLoader) throws IOException

On Android 8, the reflective call lacks the classLoader parameter, causing namespace issues. A simple workaround is to load an empty agent.so to initialize the JVMTI environment, then load the real agent with System.load.

Because JVMTI provides class redefinition, Android restricts its usage:

Only usable in debuggable apps (android:debuggable=true)

No Java API for class redefinition

Agents cannot be loaded at process start for normal apps

Not all JVMTI capabilities are implemented

Breakthrough Limitations

JVMTI is powerful, but its usefulness is limited if it only works in debug builds. To make it practical for release builds, we need to bypass the debug‑only restriction.

Limitation Principle

Agent loading consists of two steps:

EnsureJvmtiPlugin : loads libopenjdkjvmti.so and initializes the JVMTI environment.

AgentSpec.Attach : loads the agent library and uses JVMTI capabilities.

static bool EnsureJvmtiPlugin(Runtime* runtime, std::string* error_msg) {
  DCHECK(Dbg::IsJdwpAllowed() || !runtime->IsJavaDebuggable())
      << "Being debuggable requires that jdwp (i.e. debugging) is allowed.";
  if (!Dbg::IsJdwpAllowed()) {
    *error_msg = "Process is not allowed to load openjdkjvmti plugin. Process must be debuggable";
    return false;
  }
  constexpr const char* plugin_name = kIsDebugBuild ? "libopenjdkjvmtid.so" : "libopenjdkjvmti.so";
  return runtime->EnsurePluginLoaded(plugin_name, error_msg);
}

The source shows two interfaces that enforce the debug check:

Dbg::IsJdwpAllowed()
Runtime::IsJavaDebuggable()

Both flags are set in ZygoteHooks_nativePostForkChild based on the android:debuggable manifest attribute, which cannot be true for release builds.

static uint32_t EnableDebugFeatures(uint32_t runtime_flags) {
  Runtime* const runtime = Runtime::Current();
  Dbg::SetJdwpAllowed((runtime_flags & DEBUG_ENABLE_JDWP) != 0);
  if ((runtime_flags & DEBUG_ENABLE_JDWP) != 0) {
    EnableDebugger();
  }
  runtime_flags &= ~DEBUG_ENABLE_JDWP;
  bool needs_non_debuggable_classes = false;
  if ((runtime_flags & DEBUG_JAVA_DEBUGGABLE) != 0) {
    runtime->AddCompilerOption("--debuggable");
    runtime_flags |= DEBUG_GENERATE_MINI_DEBUG_INFO;
    runtime->SetJavaDebuggable(true);
    runtime->DeoptimizeBootImage();
    runtime_flags &= ~DEBUG_JAVA_DEBUGGABLE;
    needs_non_debuggable_classes = true;
  }
  return runtime_flags;
}

To bypass the restriction we modify the static variable gJdwpAllowed in libart.so via dlsym and call SetJdwpAllowed. The symbol is located by parsing /proc/self/maps and resolving the ELF.

Modify JDWP

// Source: platform/art/runtime/debugger.cc
static bool gJdwpAllowed = true;
bool Dbg::IsJdwpAllowed() { return gJdwpAllowed; }
void Dbg::SetJdwpAllowed(bool allowed) { gJdwpAllowed = allowed; }

By changing gJdwpAllowed we can make the runtime think JDWP is allowed even in a non‑debuggable app.

Modify Runtime

// Source: platform/art/runtime/runtime.h
bool is_java_debuggable_;
bool IsJavaDebuggable() const { return is_java_debuggable_; }
void Runtime::SetJavaDebuggable(bool value) { is_java_debuggable_ = value; }

We obtain the Runtime instance from the native JavaVM (via a custom JavaVMExt) and directly patch the is_java_debuggable_ field using its memory layout. Offsets are derived from stable fields such as target_sdk_version_, with additional handling for Android 12 where compat_framework_ was added.

Using Restricted Version

// A special version used to identify tooling interface versions
static constexpr jint kArtTiVersion = JVMTI_VERSION_1_2 | 0x40000000;

When JVMTI runs with this version it disables capabilities like class redefinition, but monitoring and allocation information remain available. Full functionality is available when the runtime is in a debuggable or forced‑interpret‑only mode.

TBProfiler

To make JVMTI practical for production, we wrapped its capabilities into an Android profiler called TBProfiler. Below are the main features used in the Taobao app.

Runtime Monitoring

Since Android 8.0 the Runtime exposes RuntimeCallbacks for unified event notification. JVMTI registers callbacks via an EventHandler that forwards events to the agent.

Method Call Monitoring

JVMTI provides MethodEntry and MethodExit callbacks for all method executions. Using them selectively for specific methods yields call counts, durations, and stack traces with acceptable performance impact on high‑end devices.

Thread and Class Monitoring

Thread creation/destruction and class loading generate callbacks processed in native code. This data is useful offline for analyzing class load order, supporting dex re‑ordering and PGO generation.

Exception Capture

JVMTI offers EventException and EventExceptionCatch to capture uncaught and caught exceptions. By filtering system exceptions and sampling, we can identify problematic try‑catch blocks.

Main Thread Lock Monitoring

We monitor long‑waiting locks on the main thread without hooking system methods. When a lock wait exceeds a threshold, we capture the holding thread’s stack trace. This helps pinpoint ANR and jank causes.

Locks are implemented either via ART’s Monitor (synchronized) or Java’s AQS (CLH queue + CAS). While we can directly identify the owner of a synchronized lock, AQS requires analyzing the captured information to infer waiting relationships.

Memory Monitoring

callback_->gcStart = MemoryUtils::HandleGCStart;
callback_->gcFinish = MemoryUtils::HandleGCFinish;
callback_->objectAlloc = MemoryUtils::HandleObjectAlloc;
callback_->objectFree = MemoryUtils::HandleObjectFree;

These callbacks let us track allocation size, total allocation/free volume, and GC start/end times. Large allocations trigger a BigMemoryAllocException that is reported for aggregation.

Memory Dump

Traditional hprof dumps are large (≈700 MB). By filtering out ImageSpace/ZygoteSpace objects, raw arrays, and unused fields, then compressing with zstd, we reduce the size to ~90 MB. JVMTI also enables custom, smaller dumps.

Object Instance Information

Analyzing hprof with MAT reveals instance counts, shallow size, and retained size. JVMTI can traverse reachable objects (excluding unreachable ones) to produce similar data without retained‑size information.

JVMTI Generate Hprof

Using JVMTI we can generate a standard Android hprof file by iterating over loaded classes, GC roots, class dumps, instance dumps, and array dumps via the corresponding JVMTI callbacks.

Mini Hprof

Because full hprof generation is slow, we can create a compact "Mini Hprof" containing only essential object IDs, class IDs, sizes, and reference types. This yields files under 150 KB for on‑device reporting while still allowing leak detection via reference graphs.

Summary and Outlook

Log‑based analysis alone is insufficient for memory, jank, and ANR issues. JVMTI brings us closer to the runtime, providing rich data for profiling. Future work includes integrating JVMTI‑based data into a unified profiler suite, generating custom profile files, and improving the stability and efficiency of online profiling tools.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AndroidRuntimeMemoryJVMTIprofiling
Alibaba Terminal Technology
Written by

Alibaba Terminal Technology

Official public account of Alibaba Terminal

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.