Mobile Development 15 min read

Using JVMTI to Monitor Memory Allocation and Release on Android

This article explains how to employ the Java Virtual Machine Tool Interface (JVMTI) in native Android code to record memory allocation and deallocation events, filter relevant classes, store logs efficiently with mmap, and integrate the agent from the Java layer for comprehensive memory‑leak analysis.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Using JVMTI to Monitor Memory Allocation and Release on Android

Preface

Memory management is a constant concern for developers, with issues such as OOM, leaks, and jitter being hard to detect, difficult to remediate, and prone to recurrence. Existing tools like LeakCanary address leaks from the Java side, but this article explores a VM‑side solution using JVMTI (Java Virtual Machine Tool Interface), a native API that provides extensive runtime information.

JVMTI Overview

What is JVMTI?

JVMTI is a set of native monitoring APIs supplied by the JVM. It is officially supported on Android 8.0 (API level 26) and later. Its purpose is to expose JVM events—such as thread, memory, class, and method events—through callbacks, effectively "instrumenting" the VM.

Among the many events, this guide focuses on monitoring memory allocation.

Enabling JVMTI in the Native Layer

Prerequisites

To use JVMTI you must create a native project and copy the jvmti.h header from the JDK include directory into your project's root. A memory.cpp file will host the JVMTI functions.

Agent Stub

The agent entry point is declared as:

JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);

Inside Agent_OnAttach we obtain the jvmtiEnv pointer:

// Global JVMTI environment variable
jvmtiEnv *mJvmtiEnv;

extern "C"
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
    vm->GetEnv((void **)&mJvmtiEnv, JVMTI_VERSION_1_2);
    return JNI_OK;
}

Activating Capabilities

By default JVMTI provides no capabilities. We query the potential capabilities, then enable them:

// Initialize JVMTI and enable all capabilities
jvmtiCapabilities caps;
mJvmtiEnv->GetPotentialCapabilities(&caps);
mJvmtiEnv->AddCapabilities(&caps);

Setting Event Callbacks

We configure callbacks for the memory‑related events VMObjectAlloc and ObjectFree :

jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.VMObjectAlloc = &objectAlloc;
callbacks.ObjectFree   = &objectFree;

mJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));

The allocation callback tags each object with a unique identifier and records class signatures that match our package (e.g., com/test/memory ). The deallocation callback receives only the tag and uses a list to match it with a previously recorded allocation.

void JNICALL objectAlloc(jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread,
                           jobject object, jclass object_klass, jlong size) {
    jvmti_env->SetTag(object, tag);
    tag++;
    char *classSignature;
    jvmti_env->GetClassSignature(object_klass, &classSignature, nullptr);
    if (strstr(classSignature, "com/test/memory") != nullptr) {
        // log and store tag
        __android_log_print(ANDROID_LOG_ERROR, "hello", "%s", classSignature);
        list.push_back(tag);
        char str[500];
        sprintf(str, "%s: object alloc {Tag:%lld}\r\n", classSignature, tag);
        memoryFile->write(str, strlen(str));
    }
    jvmti_env->Deallocate((unsigned char *)classSignature);
}

void JNICALL objectFree(jvmtiEnv *jvmti_env, jlong tag) {
    auto it = std::find(list.begin(), list.end(), tag);
    if (it != list.end()) {
        __android_log_print(ANDROID_LOG_ERROR, "hello", "release %lld", tag);
        char str[500];
        sprintf(str, "release tag %lld\r\n", tag);
        memoryFile->write(str, strlen(str));
    }
}

Efficient Logging with mmap

Because callbacks occur frequently, writing directly to a file would block the native thread. The MemoryFile class uses mmap to grow a shared memory region and write data without blocking.

void MemoryFile::write(char *data, int dataLen) {
    mtx.lock();
    if (currentSize + dataLen >= m_size) {
        resize(currentSize + dataLen);
    }
    memcpy(ptr + currentSize, data, dataLen);
    currentSize += dataLen;
    mtx.unlock();
}

void MemoryFile::resize(int32_t needSize) {
    int32_t oldSize = m_size;
    while (m_size < needSize) {
        m_size *= 2;
    }
    ftruncate(m_fd, m_size);
    munmap(ptr, oldSize);
    ptr = static_cast
(mmap(0, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
}

Activating the Listener

Finally, we enable the two events globally:

mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, nullptr);
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_OBJECT_FREE, nullptr);

Java Layer: Attaching the Agent

On Android 9.0+ we can call Debug.attachJvmtiAgent directly; on older versions we use reflection to invoke dalvik.system.VMDebug.attachAgent . The code copies the native .so to a path without ‘=’ characters, loads it, and registers a callback that supplies the mmap file path.

object MemoryMonitor {
    private const val JVMTI_LIB_NAME = "libjvmti-monitor.so"
    fun init(context: Context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // copy .so, load it, attach agent, set log path
        } else {
            Log.e("memory", "jvmti initialization error")
        }
    }

    private fun attachAgent(agentPath: String, classLoader: ClassLoader) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            Debug.attachJvmtiAgent(agentPath, null, classLoader)
        } else {
            val vmDebugClazz = Class.forName("dalvik.system.VMDebug")
            val attachAgentMethod = vmDebugClazz.getMethod("attachAgent", String::class.java)
            attachAgentMethod.isAccessible = true
            attachAgentMethod.invoke(null, agentPath)
        }
    }

    external fun initMemoryCallBack(path: String)
}

Verification

Running a simple activity that creates an instance of TestData generates log entries showing allocation tags and later releases, allowing developers to pinpoint classes that allocate memory without corresponding deallocation.

Conclusion

The guide demonstrates a complete JVMTI‑based memory monitoring solution on Android. While the demo omits thread‑safety and more sophisticated callbacks (e.g., method entry/exit), it provides a solid foundation for developers to extend the agent for deeper performance and leak analysis.

NativeperformanceAndroidJVMTImemory-monitoring
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login 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.