ByteBuffer无法释放内存

11
在Android上,直接字节缓冲似乎从不释放其内存,即使调用System.gc()也是如此。
例如:执行
Log.v("?", Long.toString(Debug.getNativeHeapAllocatedSize()));
ByteBuffer buffer = allocateDirect(LARGE_NUMBER);
buffer=null;
System.gc();
Log.v("?", Long.toString(Debug.getNativeHeapAllocatedSize()));

给出两个对数,第二个数字至少比第一个大LARGE_NUMBER。

我该如何消除这个泄漏?


附加:

根据Gregory的建议在C++端处理alloc/free后,我定义了

JNIEXPORT jobject JNICALL Java_com_foo_bar_allocNative(JNIEnv* env, jlong size)
    {
    void* buffer = malloc(size);
    jobject directBuffer = env->NewDirectByteBuffer(buffer, size);
    jobject globalRef = env->NewGlobalRef(directBuffer);
    return globalRef;
    }

JNIEXPORT void JNICALL Java_com_foo_bar_freeNative(JNIEnv* env, jobject globalRef)
    {
    void *buffer = env->GetDirectBufferAddress(globalRef);
    free(buffer);
    env->DeleteGlobalRef(globalRef);
    }

然后我使用以下代码在JAVA端获取我的ByteBuffer:

ByteBuffer myBuf = allocNative(LARGE_NUMBER);

并使用free函数释放它。

freeNative(myBuf);

很不幸,虽然它可以正常分配内存,但是a)它仍然会保留已分配的内存大小,根据 Debug.getNativeHeapAllocatedSize() ,b)这还会导致错误。

W/dalvikvm(26733): JNI: DeleteGlobalRef(0x462b05a0) failed to find entry (valid=1)

我现在非常困惑,原以为自己至少理解了C++方面的事情…为什么free()没有返回内存?而且我在使用DeleteGlobalRef()时做错了什么?


1
allocateDirect分配的内存不会被垃圾回收。 - Jon Willis
可以手动释放它吗?似乎很傻,有分配内存的功能却不能做相反的操作。如果可以的话,我很乐意从JNI中释放它。 - Kasper Peeters
4
allocateDirect 方法分配的内存被保证不会被垃圾回收器重新定位。但一旦 ByteBuffer 被回收,本地内存将被收回。 - Gregory Pakosz
请查看我有关 NewGlobalRefDeleteGlobalRef 的编辑后的答案。 - Gregory Pakosz
可能是 When does System.gc() do anything 的重复内容。 - Raedwald
4个回答

23

没有泄漏。

ByteBuffer.allocateDirect()从本地堆/自由存储(类似于malloc())分配内存,然后封装到ByteBuffer实例中。

ByteBuffer实例被垃圾回收时,本地内存会被回收(否则会泄漏本地内存)。

你调用System.gc()希望本地内存立即被回收。然而,调用System.gc()只是一个请求,这就解释了为什么你的第二个日志语句没有告诉你内存已经被释放:因为它还没有被释放!

在你的情况下,Java堆中显然有足够的空闲内存,垃圾收集器决定不做任何操作:因此,无法访问的ByteBuffer实例尚未被收集,它们的终结器没有运行,本地内存没有被释放。

此外,请记住JVM中的bug(不确定它如何适用于Dalvik),其中大量分配直接缓冲区会导致无法恢复的OutOfMemoryError


你提到了从JNI中进行控制的事情。实际上,这是可能的,你可以实现以下内容:
1. 发布一个名为`native ByteBuffer allocateNative(long size)`的入口点,它可以: - 调用`void* buffer = malloc(size)`来分配本地内存 - 使用`(*env)->NewDirectByteBuffer(env, buffer, size)`将新分配的数组包装成一个`ByteBuffer`实例 - ~~使用`(*env)->NewGlobalRef(env, directBuffer)`将`ByteBuffer`本地引用转换为全局引用~~
2. 发布一个名为`native void disposeNative(ByteBuffer buffer)`的入口点,它可以: - 调用`free()`释放`*(env)->GetDirectBufferAddress(env, directBuffer)`返回的直接缓冲区地址 - ~~使用`(*env)->DeleteGlobalRef(env, directBuffer)`删除全局引用~~
一旦在缓冲区上调用disposeNative,就不应再使用该引用,因此可能非常容易出错。请重新考虑是否真的需要对分配模式有如此明确的控制。
忘记我关于全局引用的说法。实际上,全局引用是一种在本地代码中存储引用的方式(就像在全局变量中),以便后续调用JNI方法可以使用该引用。例如,你可以这样做:
- 从Java中调用本地方法`foo()`,该方法将本地引用(通过从本地端创建对象获得)转换为全局引用,并将其存储在本地全局变量中(作为`jobject`)。 - 然后再次从Java中调用本地方法`bar()`,该方法获取`foo()`存储的`jobject`并进一步处理它。 - 最后,仍然从Java中调用本地方法`baz()`,以删除全局引用。
对于造成的混淆,我表示抱歉。

非常有帮助,谢谢!是的,我遇到了与虚拟机错误相关的问题。我更喜欢显式控制,因为另一种选择是分配一个估计大小的缓冲区,而那个估计值几乎总是比我实际需要的要大得多。 - Kasper Peeters
我一定是对你建议的disposeNative方法的形式有所疏漏。您介意看一下添加的注释吗?非常感谢。 - Kasper Peeters
嗯,我确实没有亲自尝试过。去掉对NewGlobalRef和DeleteGlobalRef的调用。 - Gregory Pakosz
@Gregory Pakosz:这个 bug 不知何故变得很流行。人们一遍又一遍地问同样的问题,复制这些代码而不假思索。例如,请参见 https://dev59.com/M2nWa4cB1Zd3GeqPz2Lc - Alex Cohn

1

我一直在使用TurqMage的解决方案,直到我在Android 4.0.3模拟器上测试它时(Ice Cream Sandwich)。由于某种原因,调用DeleteGlobalRef会引发jni警告:JNI WARNING: DeleteGlobalRef on non-global 0x41301ea8 (type=1),随后是分段错误。

我删除了创建NewGlobalRef和DeleteGlobalRef的调用(见下文),在Android 4.0.3模拟器上似乎可以正常工作。事实证明,我只在Java端使用了创建的字节缓冲区,其中应该包含对它的Java引用,因此我认为不需要首先调用NewGlobalRef()。

JNIEXPORT jobject JNICALL Java_com_foo_allocNativeBuffer(JNIEnv* env, jobject thiz, jlong size)
{
    void* buffer = malloc(size);
    jobject directBuffer = env->NewDirectByteBuffer(buffer, size);
    return directBuffer;
}

JNIEXPORT void JNICALL Java_comfoo_freeNativeBuffer(JNIEnv* env, jobject thiz, jobject bufferRef)
{
    void *buffer = env->GetDirectBufferAddress(bufferRef);

    free(buffer);
}

0

不确定你最后的评论是旧的还是什么,Kasper。我做了以下操作...

JNIEXPORT jobject JNICALL Java_com_foo_allocNativeBuffer(JNIEnv* env, jobject thiz, jlong size)
{
    void* buffer = malloc(size);
    jobject directBuffer = env->NewDirectByteBuffer(buffer, size);
    jobject globalRef = env->NewGlobalRef(directBuffer);

    return globalRef;
}

JNIEXPORT void JNICALL Java_comfoo_freeNativeBuffer(JNIEnv* env, jobject thiz, jobject globalRef)
{
    void *buffer = env->GetDirectBufferAddress(globalRef);

    env->DeleteGlobalRef(globalRef);
    free(buffer);
}

然后在Java中...

mImageData = (ByteBuffer)allocNativeBuffer( mResX * mResY * mBPP );

并且

freeNativeBuffer(mImageData);
mImageData = null;

对我来说一切似乎都正常工作。非常感谢Gregory提出的这个想法。JVM中引用的Bug链接已经失效。


我从未成功让Debug.getNativeHeapAllocatedSize()在free()之后报告更多的可用内存。你有这样的经历吗?我按照你写的完全一样的方式去做了。最终,我采用了一种更节省内存的解决方案,并忘记了整个“JNI端分配/释放”的事情(整个故事彻底地让我相信垃圾回收是一件坏事,但那是另外一个故事……)。 - Kasper Peeters
我也遇到了同样的问题,getNativeHeapAllocatedSize()永远不会下降。当我把那段代码放进去后,分配的大小就像预期的那样下降了。我正在使用NDK R5编译到SDK 2.1,并在运行Android 2.1的HTC Incredible上运行。 - TurqMage
你是否将 ByteBuffer 更改为其他缓冲区?我刚遇到了这个问题。我返回 ByteBuffers,使用 .asIntBuffer 并仅将它们存储为 IntBuffers。当到达释放缓冲区的时候,全局引用无法正确释放。保留对原始 ByteBuffer 的引用并释放它可以解决问题。 - TurqMage
@TurqMage 只有真正的直接字节缓冲区才能被释放,查看 OpenJDK 和 Apache Harmony 的源代码以了解原因。当您在直接字节缓冲区上创建视图时,只有被查看的缓冲区实际分配了一些内存。 - gouessej

0

使用反射调用java.nio.DirectByteBuffer.free()。我提醒您,Android DVM受Apache Harmony的启发,支持上述方法。

直接NIO缓冲区是在本地堆上分配的,而不是由垃圾回收管理的Java堆。释放其本地内存由开发人员自行处理。与OpenJDK和Oracle Java有些不同,因为它们尝试在创建直接NIO缓冲区失败时调用垃圾回收器,但不能保证其有效。

N.B:如果使用asFloatBuffer()、asIntBuffer()等,则需要多做一些努力,因为只有直接字节缓冲区可以被“释放”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接