Skip to content

Do not register class-cached FFIType memory with Cleaner - #1735

Open
arimu1 wants to merge 1 commit into
java-native-access:masterfrom
arimu1:fix-1633-ffitype-cleaner
Open

arimu1 wants to merge 1 commit into
java-native-access:masterfrom
arimu1:fix-1633-ffitype-cleaner

Conversation

@arimu1

@arimu1 arimu1 commented Aug 17, 2026

Copy link
Copy Markdown

Summary

  • FFIType descriptors for Structure classes are cached in typeInfoMap with Class keys so native ffi_cif pointers stay valid. That cache is intentional (storeTypeInfo already comments "prevent premature GC") and matches what FFITypes for non-base classes are never removed from typeInfoMap, causing the Cleaner thread to loop forever #1633 described; a Closeable/try-with-resources rewrite was rejected as too much API impact.
  • The cached backing Memory was still registered with the JNA Cleaner. Application classes never unload, so those MemoryDisposer entries never drained and the cleaner thread looped (TIMED_WAITING / RUNNABLE after Structure instances were gone).
  • Allocate that class-lifetime storage with TypeInfoMemory (no-arg Memory constructor: no Cleaner registration). Same FFIType layout and typeInfoMap cache as before.

Fixes #1633

Test plan

  • HEAD d036ad9 proved the leak: Structure-backed FFIType used AutoAllocated + Memory, both with cleanable != null.
  • After the change: TypeInfoMemory for both the FFIType struct and elements, cleanable == null; cache still returns the same pointer after the Structure instance is dropped.
  • ant -D-native=true test -Dtests.include=com/sun/jna/FFITypeTest.java — 3/3 on Temurin 21 / darwin-aarch64 (testStructureFfiTypeDoesNotRegisterCleaner, testFfiTypeSizeMatchesStructure, testStructureFfiTypeRemainsCachedAfterInstanceGc).
  • Native.initialize_ffi_type still accepts the cached descriptor (size matches the Structure).

FFIType descriptors stay in typeInfoMap for the Class lifetime so native
cif pointers remain valid. Registering that storage with the JNA Cleaner
kept the cleaner thread looping after Structure instances were collected.

Fixes java-native-access#1633
@matthiasblaesing

Copy link
Copy Markdown
Member

The core point here is this assumption:

Application classes never unload

On what basis is this claimed? Please have a look at this demo project:

UnloadableClassTest.zip

There are two classes in the project:

  • DemoClass.java: This class is loaded with a custom classloader
  • UnloadableClassTest.java: This is the Test that shall be run

After unzipping the project can be run as:

mvn process-classes exec:java

from the project directory.

The output will be:

Classloader of DemoClass: java.net.URLClassLoader@3b8aea15
==================================================
Demo ran
==================================================
Cleaning clazz
Cleaning classloader

What is happening here is:

  • a cleaner is created
  • runTest creates a new classloader (cl) based on the url derived from the class file of the UnloadableClassTest class
  • the class loader is created without a delegate, so it will load the classes itself
  • the DemoData class is loaded
  • both the classloader and the class are registered with the cleaner with runnables, that will yield a message once the cleaner for these objects is invoked
  • the classloader that actually loaded DemoData is printed (line one in the output)
  • an instance of the DemoData class is created and its run method is invoked (this prints the "Demo ran") line
  • System.gc is invoked in a loop. Once GC happens the "Cleaning ..." messages are created by the cleaner that runs the cleanup methods registered for the DemoData-Classloader and class

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FFITypes for non-base classes are never removed from typeInfoMap, causing the Cleaner thread to loop forever

2 participants