Here’s one of my favourite Java tricks when working with native libraries with manual memory management. To free native resources in tandem with the garbage collector you can use finalize(), but finalizers run on a separate thread owned by the GC, which is problematic for some libraries like OpenGL where a context can be used by one thread at a time and it’s annoying. So this trick uses finalize to revive the object just so it can have its native resources deleted on the main thread before letting the GC actually take it:

public abstract class NativeResource {
    public abstract void destroy();
    
    @Override
    protected void finalize() throws Throwable {
        NativeResourceManager.add(this);
        super.finalize();
    }
}
public class NativeResourceManager {
    private static List<NativeResource> resources = new ArrayList<>();
    
    static void add(NativeResource resource) {
        synchronized (resources) {
            resources.add(resource);
        }
    }
    
    public static void cleanup() {
        synchronized (resources) {
            for (var resource : resources) {
                resource.destroy();
            }
            
            resources.clear();
        }
    }
}

And now you have an object that can automatically annoy the garbage collector by bringing itself back from the dead to get rid of its native resources:

public class SomethingThatUsesANativeResource extends NativeResource {
    private long handle;
    
    @Override
    public void destroy() {
        destroyNativeHandle(handle);
    }
}

This is only actually useful when you can call NativeResourceManager.cleanup() constantly on the main thread, like in a game loop.

Unfortunately, finalize has been deprecated since Java 9 and deprecated for removal since Java 18 so this trick’s time is limited. Perhaps now I’ll have to actually manage my memory properly and stop trolling the GC…