JDK-8268406 : Deallocate jmethodID native memory
  • Type: Enhancement
  • Component: hotspot
  • Sub-Component: runtime
  • Affected Version: 18
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • Submitted: 2021-06-08
  • Updated: 2025-07-04
  • Resolved: 2025-06-27
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 26
26 b05Fixed
Related Reports
Relates :  
Relates :  
Relates :  
Description
Today, when a ClassLoaderData is deallocated, its JNIMethodBlocks are kept around, resulting in a slow memory leak. We do this because JVMTI agents can reuse a jmethodID that belongs to a class that has been deallocated. The JVMTI implementation tolerates this by checking if the jmethodID (which is basically a handle to a Method*) contains a NULL pointer. If so, this indicates that the jmethodID was referring to a Method that has since been deallocated. JVMTI APIs will return JVMTI_ERROR_INVALID_METHODID instead of proceeding further, e.g.,

jvmti_GetMethodDeclaringClass(jvmtiEnv* env,
            jmethodID method,
            jclass* declaring_class_ptr) {
  ...
  Method* checked_method = Method::checked_resolve_jmethod_id(method);
  if (checked_method == NULL) {
      return JVMTI_ERROR_INVALID_METHODID;
  }

However, the "regular" JNI APIs (those declared in jni.h) as implemented in HotSpot do not tolerate such invalid jmethodIDs. I have checked all functions under jni.cpp in JDK 16. Every single one of them will crash immediately when an invalid jmethodID is given, e.g., the following will crash by dereferencing the invalid pointer <m>.

JNI_ENTRY(jobject, jni_ToReflectedMethod(JNIEnv *env, jclass cls, jmethodID method_id, jboolean isStatic))
  ...
  methodHandle m (THREAD, Method::resolve_jmethod_id(method_id));
  ...
  if (m->is_initializer()) {

Therefore, we can conclude that no working JNI code today can use an invalid jmethodID without crashing (except for JVMTI agent code). As a result, we can free the JNIMethodBlocks to avoid memory leaks.
Comments
[~eosterlund] Thanks, Erik. I don't want to be a bother, I just try to understand if I should ping folks in my company working with native JNI code. I did not follow this PR closely for lack of time. I apologize for that; I should have taken the time but I was swamped. I was worried because I saw remarks like these on StackOverflow: https://stackoverflow.com/questions/2093112/why-i-should-not-reuse-a-jclass-and-or-jmethodid-in-jni, direct quotes: "Method ID and field ID values are forever" "You can store jmethodID in a static C++ variable without problems" etc. So let's see if I understand the gist of it: Before the patch: - jmethodID pointed to a slot in a JNIMethodBlockNode - These blocks are anchored in the CLD - These blocks are never released even if the CLD is deleted. However, if the CLD is deleted, the inside Method* is set to NULL. - There is also a rarely exercised path that reuses slots, but irrelevant since its jmethodID<->Method* association is never exposed to the outside With the new patch: - jmethodID is a 64-bit number that raises monotonously; jmethodIDs are never reused (the outside definition was and is a pointer, so no overflow and no binary incompatibility). - jmethodID and Method* are stored in a global hash map, with jmethodID the key - If the CLD is deleted, these entries are removed and memory for them free'd. - Querying the hash table for a deleted entry results in a NULL Method* If all of this is correct, this should be true before and after patch: - A jmethodID is unique for the lifetime of the JVM. - If its underlying class was unloaded, the associated Method* is NULL and using it results in SIGSEGVs. If my understanding is correct, then I stop worrying. I was worried about jmethodID lifetime changes and reuses. But it seems behavior for these cases is unchanged. In that case I retract my request for a release note, since with no observable behavior change I would not be sure what to tell people. I agree that any code making assumptions about the underlying Method* is probably very rare. The only hypothetical case I could come up with is if someone dereferences a jmethodID in order to see if its Klass was unloaded. But I admit even that is far-fetched and one cannot cater to all misuses.
04-07-2025

I don’t think this needs a release note. we changed jweak and jobject representations with no release note. It’s up to the JVM what these handles encode, by design. Fetching the underlying Method* is an abstraction crime. What are you going to do with a JVM internal Method* anyway? Read fields that you happen to know are at index 0x28 and 0x30? That on its own is also a huge abstraction crime. We can’t write release notes when we change this kind of obviously private implementation details IMO.
04-07-2025

Hi [~apangin] thanks for filing the issue and the fast fix : https://github.com/async-profiler/async-profiler/commit/f878b25a2f84d8bced13485695d0609aba2fcb02
03-07-2025

[~apangin] Thanks for filing that issue. Yes, jmethodIDs no longer point to Methods and native code shouldn't expect them to do so. I hope the async profiler can be fixed not to do this. Also, there may be incompatibilities if external native code expects this implementation. You should be calling back into the VM to tell you things about the Method*.
01-07-2025

[~mbaesken] I believe this incompatibility should be fixed in async-profiler, I created an issue to track this: https://github.com/async-profiler/async-profiler/issues/1358
01-07-2025

Hi [~coleenp] starting from this change, we run into crashes with asyncprofiler . https://github.com/async-profiler/async-profiler/releases/download/v4.0/async-profiler-4.0-linux-x64.tar.gz was used for testing (e.g. on Linux x86_64) Example stack : Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) C [libasyncProfiler.so+0x1a0be] FrameName::javaMethodName(_jmethodID*)+0x1e C [libasyncProfiler.so+0x31721] FrameName::name(ASGCT_CallFrame&, bool) [clone .constprop.1253]+0x71 C [libasyncProfiler.so+0x32a69] Profiler::dumpText(Writer&, Arguments&)+0x5f9 C [libasyncProfiler.so+0x3ebae] Profiler::dump(Writer&, Arguments&)+0x2be C [libasyncProfiler.so+0x439df] Profiler::run(Arguments&)+0x21f C [libasyncProfiler.so+0x444be] VM::VMDeath(_jvmtiEnv*, JNIEnv_*)+0x4e V [libjvm.so+0xc19c28] JvmtiExport::post_vm_death()+0x1f8 (jvmtiExport.cpp:777) V [libjvm.so+0x9f6c75] before_exit(JavaThread*, bool)+0x315 (java.cpp:508) V [libjvm.so+0xadeeb9] JVM_Halt+0x69 (jvm.cpp:431) j java.lang.Shutdown.halt0(I)V+0 java.base@26.0.0.1-internal j java.lang.Shutdown.halt(I)V+7 java.base@26.0.0.1-internal j java.lang.Shutdown.exit(I)V+16 java.base@26.0.0.1-internal j java.lang.Terminator$1.handle(Ljdk/internal/misc/Signal;)V+8 java.base@26.0.0.1-internal j jdk.internal.misc.Signal$1.run()V+8 java.base@26.0.0.1-internal j java.lang.Thread.runWith(Ljava/lang/Object;Ljava/lang/Runnable;)V+5 java.base@26.0.0.1-internal j java.lang.Thread.run()V+19 java.base@26.0.0.1-internal v ~StubRoutines::call_stub 0x00007f3db700d6a2 V [libjvm.so+0x9f83b0] JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)+0x2b0 (javaCalls.cpp:415) V [libjvm.so+0x9f9d1f] JavaCalls::call_virtual(JavaValue*, Handle, Klass*, Symbol*, Symbol*, JavaThread*)+0x1df (javaCalls.cpp:323) V [libjvm.so+0xadeacc] thread_entry(JavaThread*, JavaThread*)+0x8c (jvm.cpp:2748) V [libjvm.so+0xa0ea68] JavaThread::thread_main_inner() [clone .part.0]+0xb8 (javaThread.cpp:773) V [libjvm.so+0x1103b0f] Thread::call_run()+0x9f (thread.cpp:243) V [libjvm.so+0xe1e6f6] thread_native_entry(Thread*)+0xd6 (os_linux.cpp:868) Java frames: (J=compiled Java code, j=interpreted, Vv=VM code) j java.lang.Shutdown.halt0(I)V+0 java.base@26.0.0.1-internal j java.lang.Shutdown.halt(I)V+7 java.base@26.0.0.1-internal j java.lang.Shutdown.exit(I)V+16 java.base@26.0.0.1-internal j java.lang.Terminator$1.handle(Ljdk/internal/misc/Signal;)V+8 java.base@26.0.0.1-internal j jdk.internal.misc.Signal$1.run()V+8 java.base@26.0.0.1-internal j java.lang.Thread.runWith(Ljava/lang/Object;Ljava/lang/Runnable;)V+5 java.base@26.0.0.1-internal j java.lang.Thread.run()V+19 java.base@26.0.0.1-internal v ~StubRoutines::call_stub 0x00007f3db700d6a2 siginfo: si_signo: 11 (SIGSEGV), si_code: 1 (SEGV_MAPERR), si_addr: 0x0000000000001d00
01-07-2025

> Will this not break JVMTI? Hi [~jbachorik] at least we see now issues with async-profiler, after the change came into jdk26.
01-07-2025

How I saw the crash I reported : 1. get https://github.com/async-profiler/async-profiler/releases/download/v4.0/async-profiler-4.0-linux-x64.tar.gz and unpack it 2. copy libasyncProfiler.so into the jvm you want to test , e.g. cp async-profiler-4.0-linux-x64/lib/libasyncProfiler.so /my-jdk-head-image-dir/images/jdk/lib 3. start the JVM from /my-jdk-head-image-dir/images/jdk on a simple mini-program that just loops or does some other simple stuff : ./images/jdk/bin/java -agentlib:asyncProfiler=start,flat=10000,interval=50us,traces=1,event=wall,loglevel=none LoopProg 4. end the 'LoopProg' with CTRL-C and the crash happens ; before the change it does not crash
01-07-2025

Changeset: d8f9b188 Branch: master Author: Coleen Phillimore <coleenp@openjdk.org> Date: 2025-06-27 11:20:49 +0000 URL: https://git.openjdk.org/jdk/commit/d8f9b188fa488c9c6e343c62a148cfe9fc8a563b
27-06-2025

A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk/pull/25267 Date: 2025-05-16 12:18:42 +0000
12-06-2025

The PoC that I'm working on (which needs performance testing), will look the same as current jmethodIDs. Ie. if the class is unloaded and you try to get the Method from the jmethodID, it will return nullptr, otherwise will return the Method. It just finds it or doesn't find it in a hashtable, and it's not an indirection. https://github.com/openjdk/jdk/pull/14575
08-08-2023

Will this not break JVMTI? We are already having issues with working with stacktraces created by JVMTI - https://bugs.openjdk.org/browse/JDK-8313816. If the jmethodids get deallocated working with JVMTI stacktraces will be a walk in minefield. And situation will be similar for stacktraces collected by ASGCT - there is no practical way to 'lock' all classes which are referenced from a stacktrace in order to make sure the jmethodids will remain valid. Therefore, any attempt to resolve the jmethodid from such stacktrace can crash JVM. Currently, we are trying to work this around by having jmethodID->method metadata cache but this kind of futile endeavour because we can not get any information about class being unloaded so we need to either keep all jmethodids forever and risk OOME or just apply an arbitrary retention, trading the data (we will not be able to resolve old jmethodids) for safety (no JVM crashes).
08-08-2023

[~jiangli] Do you have good performance tests for jmethodID creation?
28-07-2023

Need to make a non-racy jmethodID cache to map Method->jmethodID in order to implement the hashtable idea, so linking to the cleanup JDK-8313332.
28-07-2023

I am not sure how much leak we have in real world programs: - I ran Eclipse and I found jmethodIDs created in custom loaders by JNI code (but a total of only 600 jmethodIDs were created for all loaders). - But I ran a SpringBoot helloworld program, which uses a bunch of custom loaders, and it never creates jmethodIDs for custom loaders. So perhaps we will have a non-trivial memory leak only when running with JVMTI enabled.
08-06-2021

> As an implementation, we need to decide whether the benefit of this RFE (fixing memory leaks) is worth the risk of potential arbitrary execution caused by buggy JNI code. Some input based on my observation: Most the cases that create jmethod IDs are related to JVMTI agents. Agents are more commonly enabled in real world usages in production.
08-06-2021

Note: today jmethodIDs are allocated from the C heap. When a jmethodID becomes invalid (i.e., its class has been deallocated), we keep the jmethodID around, but set its content to NULL. As a result, trying to use such an jmethodID with JNI will predictably lead to a NULL dereference and thus a guaranteed crash. By implementing this RFE, the jmethodID are freed, and its memory can potentially be reused. So we can no longer guarantee the NULL dereference if JNI code uses an "old" jmethodID. The spec allows this: https://docs.oracle.com/en/java/javase/16/docs/specs/jni/design.html#reporting-programming-errors "The programmer must not pass illegal pointers or arguments of the wrong type to JNI functions. Doing so could result in arbitrary consequences, including a corrupted system state or VM crash." As an implementation, we need to decide whether the benefit of this RFE (fixing memory leaks) is worth the risk of potential arbitrary execution caused by buggy JNI code. Risk mitigation: [1] We can keep the existing behavior (do not free jmethodID but NULL its content) when -Xcheck:jni is enabled [2] Write a random value into the jmethodID before freeing it
08-06-2021