java自动回收C++内存

419 阅读4分钟

背景

JVM本身是具有自动内存回收的功能的,但是在用到了JNI(Java Native Interface)的场景下,自定义的native方法中申请的内存JVM是管理不到的,因此需要自行清理。因此如果想释放相应的内存,就需要显式调用相应的析构方法。

考虑到在实际开发过程中,如果要求开发人员每次使用完后,自行进行显式释放,会十分的不雅观(不够高大上)。同时如果相关代码被封装成一个模块供其他模块使用,那同时也会要求别的模块进行显式的调用相应的垃圾回收方法,这样会导致耦合性很强。

可行方案

因此,可以利用JVM提供的相关功能支持,跟JVM的GC线程一样,自动的回收C++内存。目前可行的方法有两种:

1、让绑定了C++内存的Java对象重写Object的finalize方法。
2、另起一个线程,专门负责检查与释放C++内存。

finalize方法

public abstract class NativeObject {
    /**
     * 自定义的清理native内存的方法
     */
    public abstract void free();

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        free();
    }
}

class Cat extends NativeObject {
    @Override
    public native void free();
}

通过重写Object的finalize方法,在finalize方法里面调用释放内存的native方法。这样就可以做到,在GC回收NativeObject类之前,释放掉JNI接口中释放的内存。

这种方法的好处是,实现起来十分的简单。从上面的代码可以看出,只要涉及C++内存释放的类都继承NativeObject,然后编写对应的C++内存释放方法就可以了。内存释放的时机完全交给JVM的GC来决定。

但这种方法的弊端是远远大于其收益的:

finalize方法废弃的官方注释

image.png 从Java9开始,该方法就被标注为了deprecate状态。

从官方的说法中我们可以看出finalize方法存在很多问题,同时官方建议如果对象持有了非堆内内存时,应当实现AutoCloseable接口,并显式的进行释放。另外可以通过Cleaner和PhantomReference来进行高效灵活的资源释放。

Reference类

Reference类有三个常见的子类,分别是SoftReference、WeakReference与PhantomReference。它们的区别这里就不展开讨论。

Cleaner类

image.png 官方的文档中说明了,Cleaner类是通过PhantomReference(虚引用)来实现的,通过register方法将目标对象和对应的清理方法注册进Cleaner中,当目标对象判断为不可达时,Cleaner将会调用清理方法对目标对象进行后续清理。

public class CleaningExample implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    private final Cleaner.Cleanable task;

    static class CleanTask implements Runnable {

        @Override
        public void run() {
            // delete native memory
        }

    }
    public CleaningExample() {
        task = CLEANER.register(this, new CleanTask());
    }

    @Override
    public void close() throws Exception {
        task.clean();
    }
}

上面的例子是java.lang.ref.Cleaner类注释中提供的一个例子,该方法主要的方法有两个

create image.png create方法的作用是创建一个Cleaner实例,每一个Cleaner实例都会启动一个线程来执行清理任务。

register

image.png register方法有两个参数,第一个参数表示的是需要监控的对象,第二个参数表示的是,当obj(被监控对象)不可达时执行的动作。而register方法会有一个Cleanable类型的返回值,Cleanable的对象会有clean的方法来调用action。这就意味着我们可以通过返回的Cleanable对象来进行显式的调用action,但如果action没有被显式调用则在被监控对象不可达时自动调用以达到清理效果。

DirectByteBuffer是如何回收堆外内存的

除了java.lang.ref.Cleaner类注释中提供的案例之外,我们也可以参考DirectByteBuffer中的堆外内存处理方式。

image.png 从DirectByteBuffer中的构造器中可以发现,DirectByteBuffer用的堆外内存回收方式也是通过Cleaner的方式,但是DirectByteBuffer中用到的Cleaner与finalize方法中官方给出的Cleaner类不是同一个,但是具体功能是一样的。感兴趣的朋友可以自己去研究下,DirectByteBuffer用的是jdk.internal.ref.Cleaner,官方推荐的是java.lang.ref.Cleaner。

image.png 其中Deallocator类则是DirectByteBuffer对象不可达时的内存清理任务,主要的清理逻辑为89行的freeMemory,而address则对应的是C++中的内存地址。91行是用于堆外内存统计管理。freeMemory最终会调用一个由C++实现的native方法,来对传入的内存地址进行释放。

image.png

image.png