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.
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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.