ARTICLE AD BOX
I have a Java application that calls a native function. That native function has a loop and in each iteration launches 10 threads and waits for them to complete. Each thread just does a number of random memory allocations that are then freed again on the same thread. After each iteration I track the memory reports (VmRss from /proc/PID/status and fields from /proc/$PID/statm).
If I do not call AttachCurrentThread() in the threads then memory consumption stays flat (as expected). If I do call AttachCurrentThread() at the beginning of each thread and DetachCurrentThread() at the end of each thread then it seems like there is a memory leak: the numbers reported for VmRSS and the "resident" column in /proc/$PID/statm are going up.
Is there more I have to do properly detach the thread from the JVM? Is the JVM retaining memory for some reason? My actual application is much bigger than the code I provide here and eventually runs out of memory - apparently due to the leak I am observing here.
My JVM is
$ java -version openjdk version "21.0.9" 2025-10-21 OpenJDK Runtime Environment (build 21.0.9+10-Ubuntu-124.04) OpenJDK 64-Bit Server VM (build 21.0.9+10-Ubuntu-124.04, mixed mode, sharing)and with the code below I did
make run make run ARGS=--no-attachJava code:
public final class Leak { static { System.loadLibrary("library"); } public static native void randomAllocations(boolean attach); public static void main(String[] args) { boolean attach = true; for (String arg : args) { if (arg.equals("--no-attach")) attach = false; else throw new RuntimeException("Unknown argument " + arg); } randomAllocations(attach); } }Native code:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <pthread.h> #include <jni.h> #define threads 10 /* Number of threads to use. */ #define count 10000 /* Number of memory allocations in each thread. */ #define repeats 60 /* Number of repetitions of main loop. */ static JavaVM *jvm = NULL; /* Java virtual machine (set in native function). */ /* Dump statistics read from /proc to CSV file. */ static void dumpStats(FILE *out) { long long const pid = getpid(); char filename[64]; char line[256]; char *endptr; FILE *f; long VmRSS, VmHWM; long size, resident, shared, text, lib, data, dt; snprintf(filename, sizeof(filename), "/proc/%lld/status", pid); if ( (f = fopen(filename, "r")) == NULL ) { perror("fopen"); abort(); } while (fgets(line, sizeof(line), f)) { endptr = NULL; if ( strncmp(line, "VmRSS:", 6) == 0 ) VmRSS = strtol(line + 6, &endptr, 0); else if ( strncmp(line, "VmHWM:", 6) == 0 ) VmHWM = strtol(line + 6, &endptr, 0); if ( endptr && (endptr[0] != ' ' || endptr[1] != 'k' || endptr[2] != 'B') ) { perror("strtol"); abort(); } } fclose(f); snprintf(filename, sizeof(filename), "/proc/%lld/statm", pid); if ( (f = fopen(filename, "r")) == NULL ) { perror("fopen"); abort(); } if ( !fgets(line, sizeof(line), f) ) { perror("fgets"); abort(); } if ( sscanf(line, "%ld %ld %ld %ld %ld %ld %ld", &size, &resident, &shared, &text, &lib, &data, &dt) != 7 ) { perror("sscanf"); abort(); } fclose(f); fprintf(out, "%ld,%ld,%ld,%ld,%ld,%ld,%ld,%ld,%ld\n", VmHWM, VmRSS, size, resident, shared, text, lib, data, dt); fflush(out); } /* Per thread data. */ typedef struct { unsigned int seed; /* Random seed for this thread. */ jboolean attach; /* Whether thread should attach to JVM. */ pthread_t thread; /* Thread handle. */ } ThreadData; static int sizes[1024]; /* Random sizes for memory blocks to allocate. */ static void *thread(void *data) { ThreadData const *d = data; JNIEnv *javaenv = NULL; jint r; int ret; if ( d->attach && (ret = (*jvm)->AttachCurrentThread(jvm, (void **)&javaenv, NULL)) ) { fprintf(stderr, "#### ATTACH FAILED: %d\n", ret); abort(); } { /* Simulate some random memory allocations. */ void **pointers = NULL; pointers = malloc(sizeof(*pointers) * count); if ( !pointers ) { fprintf(stderr, "#### MALLOC failed\n"); abort(); } for (jint i = 0; i < count; ++i) { size_t bytes = (d->seed + i) % (sizeof(sizes) / sizeof(sizes[0])); pointers[i] = malloc(bytes); } for (jint i = 0; i < count; ++i) { free(pointers[i]); } free(pointers); } if ( d->attach && (ret = (*jvm)->DetachCurrentThread(jvm)) != 0 ) { fprintf(stderr, "#### DETACH FAILED: %d\n", ret); abort(); } return NULL; } JNIEXPORT void JNICALL Java_Leak_randomAllocations(JNIEnv *javaenv, jclass clazz, jboolean attach) { int ret; int jret = 0; FILE *f; char filename[256]; unsigned u; (void)javaenv; (void)clazz; snprintf(filename, sizeof(filename), "stats_%s_%lld.csv", attach ? "attach" : "no_attach", (long long)getpid()); if ( (f = fopen(filename, "w")) == NULL ) { perror("fopen"); abort(); } fprintf(f, "VmHWM,VmRSS,size,resident,shared,text,lib,data,dt\n"); dumpStats(f); ret = (*javaenv)->GetJavaVM(javaenv, &jvm); if ( ret ) { fprintf(stderr, "#### FAILED TO GET JVM: %d\n", ret); abort(); } srand(0); for (u = 0; u < sizeof(sizes) / sizeof(sizes[0]); ++u) sizes[u] = rand() % (1024 * 1024 * 4); for (jint r = 0; r < repeats; ++r) { ThreadData *data = malloc(threads * sizeof(*data)); jint t = 0; if ( !data ) { fprintf(stderr, "#### BASIC ALLOCATION FAILED: %d\n", ret); abort(); } for (t = 0; t < threads; ++t) { data[t].seed = t; data[t].attach = attach; if ( pthread_create(&data[t].thread, NULL, thread, &data[t]) ) { fprintf(stderr, "#### THREAD CREATE FAILED\n"); abort(); } } for (t = 0; t < threads; ++t) { pthread_join(data[t].thread, NULL); } free(data); dumpStats(f); } fclose(f); }Makefile:
.PHONY: all clean run all: liblibrary.so Leak.class clean: rm -f liblibrary.so Leak.class run: liblibrary.so Leak.class java -cp . -Djava.library.path=. Leak $(ARGS) liblibrary.so: library.c $(CC) -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux -shared -fPIC -o $@ $< Leak.class: Leak.java javac Leak.java
