【问题标题】:ByteBuffer not releasing memoryByteBuffer 不释放内存
【发布时间】:2011-06-30 22:12:54
【问题描述】:

在 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() 做错了什么?

【问题讨论】:

  • allocateDirect 分配保证不会被垃圾回收的内存。
  • 那很好,但我可以手动释放它吗?具有分配内存的功能但不能相反的功能似乎很愚蠢。如果可以的话,我很乐意将它从 JNI 中解放出来。
  • Jon > allocateDirect 分配的内存保证不会被垃圾收集器重定位。但是一旦ByteBuffer 被收集,本机内存就会被回收。
  • 查看我编辑的关于NewGlobalRefDeleteGlobalRef的答案

标签: android memory-leaks java-native-interface bytebuffer


【解决方案1】:

没有泄漏。

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 实例中
    • ByteBuffer 本地引用转换为带有(*env)->NewGlobalRef(env, directBuffer); 的全局引用
  2. 发布一个native void disposeNative(ByteBuffer buffer) 入口点:

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

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


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

  • 从 Java 调用本地方法 foo(),它从本地引用(通过从本地创建对象获得)创建全局引用并将其存储在本地全局变量中(作为 jobject
  • 再次从 Java 调用原生方法 bar(),该方法获取 foo() 存储的 jobject 并进一步处理它
  • 最后,还是来自 Java,最后一次调用原生 baz() 删除了全局引用

很抱歉给您带来了困惑。

【讨论】:

  • 这非常有帮助,谢谢!是的,我遇到了与 VM 错误相关的问题。我更喜欢有明确的控制,因为另一种方法是分配一个估计大小的缓冲区,而这个估计几乎总是比我实际需要的大得多。
  • 我一定遗漏了您建议的 disposeNative 形式。你介意看看添加的评论吗?非常感谢。
  • 嗯,我自己确实没有尝试过。删除对 NewGlobalRef 和 DeleteGlobalRef 的调用
  • @Gregory Pakosz:这个错误不知何故传播开来。人们不断地问同样的问题,不假思索地复制了这些台词。参见例如stackoverflow.com/questions/12803493/…
【解决方案2】:

我一直在使用 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);
}

【讨论】:

    【解决方案3】:

    不确定您最后的 cmets 是旧的还是 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 的链接已损坏。

    【讨论】:

    • 我从未设法让 Debug.getNativeHeapAllocatedSize() 在 free() 之后报告更多可用内存。它对你这样做吗?我做了和你写的完全一样的事情。最后,我找到了一个内存较少的解决方案,并忘记了整个“JNI 端的 alloc/dealloc”的事情(整个故事让我彻底相信垃圾收集是一件坏事,但那是另一回事了。 .)
    • 是的,我遇到了同样的问题,getNativeHeapAllocatedSize() 永远不会失败。当我将该代码放入 Alloc'd 时,大小会按预期下降。我正在使用 NDK R5 编译到 SDK 2.1,并在运行 Android 2.1 的 HTC Incredible 上运行。
    • 您是否将 ByteBuffer 更改为另一个缓冲区?我刚刚遇到了这个问题。我正在返回 ByteBuffers,使用 .asIntBuffer 并将它们仅存储为 IntBuffers。当需要释放缓冲区时,GlobalRefs 将无法正确释放。保留对原始 ByteBuffer 的引用并释放它解决了问题。
    • @TurqMage 只能释放真正的直接字节缓冲区,看看 OpenJDK 和 Apache Harmony 的源代码就明白为什么了。当您在直接字节缓冲区上创建视图时,只有查看的缓冲区才会真正分配一些内存。
    【解决方案4】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-06-29
      • 2016-09-14
      • 2015-08-19
      • 2012-05-15
      • 2018-01-26
      • 2011-09-30
      • 2012-03-18
      • 2011-08-10
      相关资源
      最近更新 更多