11

在 Android 上,直接 ByteBuffer 似乎永远不会释放其内存,即使在调用 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);

并释放它

freeNative(myBuf);

不幸的是,虽然它确实分配得很好,但它 a) 仍然保持根据分配的内存Debug.getNativeHeapAllocatedSize()b) 导致错误

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

我现在完全糊涂了,我以为我至少理解了 C++ 方面的东西......为什么 free() 不返回内存?我做错了DeleteGlobalRef()什么?

4

4 回答 4

23

没有泄漏。

ByteBuffer.allocateDirect()从本机堆/空闲存储(think malloc())分配内存,然后将其包装到ByteBuffer实例中。

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

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

在您的情况下,Java 堆中显然有足够的空闲内存,而垃圾收集器决定什么都不做:因此,ByteBuffer尚未收集无法访问的实例,它们的终结器未运行,并且未释放本机内存。

另外,请记住 JVM 中的这个错误(虽然不确定它如何应用于 Dalvik),直接缓冲区的大量分配导致不可恢复的OutOfMemoryError.


您评论了从 JNI 进行控制。这实际上是可能的,您可以实现以下内容:

  1. 发布一个native ByteBuffer allocateNative(long size)入口点:

    • 调用void* buffer = malloc(size)分配本机内存
    • 将新分配的数组包装到一个ByteBuffer实例中,并调用(*env)->NewDirectByteBuffer(env, buffer, size);
    • ByteBuffer本地引用转换为全局引用(*env)->NewGlobalRef(env, directBuffer);
  2. 发布一个native void disposeNative(ByteBuffer buffer)入口点:

    • 调用free()返回的直接缓冲区地址*(env)->GetDirectBufferAddress(env, directBuffer);
    • 删除全局引用(*env)->DeleteGlobalRef(env, directBuffer);

一旦你调用disposeNative了缓冲区,你就不应该再使用这个引用了,所以它很容易出错。重新考虑您是否真的需要对分配模式进行这种显式控制。


忘记我所说的关于全局引用的内容。实际上,全局引用是一种在本机代码中存储引用的方法(例如在全局变量中),以便进一步调用 JNI 方法可以使用该引用。因此,您将拥有例如:

  • 从 Java 调用本地方法foo(),该方法从本地引用创建全局引用(通过从本地创建对象获得)并将其存储在本地全局变量中(作为 a jobject
  • 再次从 Java 中返回,调用本地方法bar(),该方法获取jobject存储foo()并进一步处理它
  • 最后,仍然来自 Java,最后一次调用 nativebaz()会删除全局引用

对困惑感到抱歉。

于 2011-02-20T23:02:00.140 回答
1

我一直在使用 TurqMage 的解决方案,直到我在 Android 4.0.3 模拟器(冰淇淋三明治)上对其进行了测试。由于某种原因,对 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);
}
于 2012-03-27T14:08:41.950 回答
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;

一切似乎对我来说都很好。非常感谢格雷戈里的这个想法。JVM 中引用的 Bug 的链接已损坏。

于 2011-03-14T22:53:28.617 回答
0

使用反射调用java.nio.DirectByteBuffer.free()。我提醒您,Android DVM 的灵感来自 Apache Harmony,它支持上述方法。

直接 NIO 缓冲区分配在本机堆上,而不是在垃圾回收管理的 Java 堆上。由开发人员来释放他们的本机内存。这与 OpenJDK 和 Oracle Java 有点不同,因为当直接 NIO 缓冲区的创建失败但不能保证它会有所帮助时,它们会尝试调用垃圾收集器。

注意:如果你使用 asFloatBuffer(), asIntBuffer(), ... 因为只有直接字节缓冲区可以被“释放”,你将不得不做更多的修改。

于 2015-09-15T13:15:05.200 回答