ByteBuffer не освобождает память

В 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 больше первого.

Как мне избавиться от этой утечки?


Добавлено:

Следуя предложению Грегори обрабатывать alloc / free на стороне C ++, я затем определил

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);
    }

Затем я получаю свой ByteBuffer на стороне JAVA с помощью

ByteBuffer myBuf = allocNative(LARGE_NUMBER);

и освободи его с помощью

freeNative(myBuf);

К сожалению, несмотря на то, что он выделяет нормально, он а) по-прежнему сохраняет выделенную память в соответствии с Debug.getNativeHeapAllocatedSize() и б) приводит к ошибке

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

Я сейчас полностью запутался, я думал, что, по крайней мере, понял сторону С ++ ... Почему free () не возвращает память? А что я делаю не так с DeleteGlobalRef()?


person Kasper Peeters    schedule 20.02.2011    source источник
comment
allocateDirect выделяет память, которая гарантированно не будет собираться сборщиком мусора.   -  person Jon Willis    schedule 21.02.2011
comment
Ничего страшного, но можно как-нибудь освободить его вручную? Кажется глупым иметь возможность выделять память, но не иметь возможности делать обратное. Я счастлив, что освободил его от JNI, если это возможно.   -  person Kasper Peeters    schedule 21.02.2011
comment
Джон ›allocateDirect выделяет память, которая гарантированно не будет перемещена сборщиком мусора. Но как только ByteBuffer будет собран, будет возвращена собственная память.   -  person Gregory Pakosz    schedule 21.02.2011
comment
см. мой отредактированный ответ о NewGlobalRef и DeleteGlobalRef   -  person Gregory Pakosz    schedule 21.02.2011
comment
возможный дубликат Когда System.gc () что-то делает   -  person Raedwald    schedule 24.12.2014


Ответы (4)


Утечки нет.

ByteBuffer.allocateDirect() выделяет память из собственной кучи / свободного хранилища (думаю, malloc()), которая, в свою очередь, обернута в ByteBuffer экземпляр.

Когда экземпляр ByteBuffer получает сборщик мусора, восстанавливается собственная память (в противном случае произошла бы утечка собственной памяти).

Вы вызываете System.gc() в надежде, что внутренняя память будет освобождена немедленно. Однако вызов System.gc() - это всего лишь запрос, который объясняет, почему ваш второй оператор журнала не сообщает вам, что память была освобождена: это потому, что это еще не сделано!

В вашей ситуации очевидно достаточно свободной памяти в куче Java, и сборщик мусора решает ничего не делать: как следствие, недостижимые ByteBuffer экземпляры еще не собираются, их финализатор не запускается и собственная память не освобождается.

Также помните об этой ошибке в JVM (хотя не знаю, как это применимо к Dalvik) где чрезмерное выделение прямых буферов приводит к невосстановимому OutOfMemoryError.


Вы прокомментировали управление вещами из JNI. На самом деле это возможно, вы можете реализовать следующее:

  1. опубликовать native ByteBuffer allocateNative(long size) точку входа, которая:

    • calls void* buffer = malloc(size) to allocate native memory
    • оборачивает вновь выделенный массив в экземпляр ByteBuffer с вызовом (*env)->NewDirectByteBuffer(env, buffer, size);
    • преобразует ByteBuffer локальную ссылку в глобальную с (*env)->NewGlobalRef(env, directBuffer);
  2. опубликовать native void disposeNative(ByteBuffer buffer) точку входа, которая:

    • calls free() on the direct buffer address returned by *(env)->GetDirectBufferAddress(env, directBuffer);
    • удаляет глобальную ссылку с (*env)->DeleteGlobalRef(env, directBuffer);

Как только вы вызываете disposeNative в буфере, вы не должны больше использовать ссылку, поэтому это может быть очень подвержено ошибкам. Еще раз подумайте, действительно ли вам нужен такой явный контроль над шаблоном распределения.


Забудьте, что я сказал о глобальных ссылках. На самом деле глобальные ссылки - это способ сохранить ссылку в собственном коде (например, в глобальной переменной), так что дальнейший вызов методов JNI может использовать эту ссылку. Например, у вас будет:

  • из Java вызовите собственный метод foo(), который создает глобальную ссылку из локальной ссылки (полученной путем создания объекта на собственной стороне) и сохраняет ее в собственной глобальной переменной (как jobject)
  • еще раз, снова из Java, вызовите собственный метод bar(), который получает jobject, хранящийся в foo(), и обрабатывает его.
  • наконец, все еще из Java, последний вызов native baz() удаляет глобальную ссылку

Извините за путаницу.

person Gregory Pakosz    schedule 20.02.2011
comment
Это очень полезно, спасибо! Да, у меня возникла проблема, связанная с ошибкой виртуальной машины. Я предпочитаю иметь явный контроль, поскольку альтернативой является выделение буфера с предполагаемым размером, и эта оценка почти всегда намного больше, чем то, что мне действительно нужно. - person Kasper Peeters; 21.02.2011
comment
Должно быть, мне что-то не хватает в предложенной вами форме disposeNative. Не могли бы вы взглянуть на добавленный комментарий? Большое спасибо. - person Kasper Peeters; 21.02.2011
comment
хм, я и сам не пробовал. удалить вызовы NewGlobalRef и DeleteGlobalRef - person Gregory Pakosz; 21.02.2011
comment
@Gregory Pakosz: эта ошибка каким-то образом стала вирусной. Люди задают одни и те же вопросы, не задумываясь скопировав эти строки. См., Например, stackoverflow.com/questions/12803493/ - person Alex Cohn; 10.10.2012

Я использовал решение TurqMage, пока не протестировал его на эмуляторе Android 4.0.3 (Ice Cream Sandwich). По какой-то причине вызов DeleteGlobalRef завершается ошибкой с предупреждением jni: JNI WARNING: DeleteGlobalRef на неглобальном 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);
}
person Gino    schedule 27.03.2012

Не уверен, что ваши последние комментарии старые или какие 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);
}

Потом на Яве ...

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

и

freeNativeBuffer(mImageData);
mImageData = null;

и вроде у меня все работает нормально. Большое спасибо Грегори за эту идею. Ссылка на указанную ошибку в JVM испортилась.

person TurqMage    schedule 14.03.2011
comment
Мне так и не удалось заставить Debug.getNativeHeapAllocatedSize () сообщать о большем количестве свободной памяти после free (). Это делает это для вас? Я поступил точно так же, как вы написали. В конце концов, я выбрал решение, которое требует меньше памяти, и забыл обо всей этой штуке с «выделением / освобождением на стороне JNI» (вся история полностью убедила меня, что сборка мусора - это плохо, но это уже другая история ... .) - person Kasper Peeters; 15.03.2011
comment
Да, у меня была такая же проблема, когда getNativeHeapAllocatedSize () никогда не падал. Когда я поместил этот код в размер Alloc'd, размер упал, как и следовало ожидать. Я компилирую SDK 2.1 с NDK R5 и использую HTC Incredible под управлением Android 2.1. - person TurqMage; 15.03.2011
comment
Вы меняли ByteBuffer на другой буфер? Я просто столкнулся с этой проблемой. Я возвращал ByteBuffers, используя .asIntBuffer и хранил их только как IntBuffers. Когда пришло время освободить буферы, GlobalRefs не освободился бы правильно. Сохранение ссылки на исходный ByteBuffer и освобождение от него устранили проблему. - person TurqMage; 15.03.2011
comment
@TurqMage Может быть освобожден только настоящий прямой байтовый буфер, посмотрите исходный код OpenJDK и Apache Harmony, чтобы понять почему. Когда вы создаете представление в прямом байтовом буфере, только просматриваемый буфер действительно выделяет некоторую память. - person gouessej; 15.09.2015

Используйте отражение для вызова