JNA / ByteBuffer не освобождается и приводит к нехватке памяти кучи C - PullRequest
11 голосов
/ 16 ноября 2009

Позвольте мне начать с того, что мое понимание того, как JNA и Java направляют собственное распределение памяти в лучшем случае, является в лучшем случае внутренним, поэтому я пытаюсь описать свое понимание того, что происходит. Любые исправления в дополнение к ответам были бы великолепны ...

Я запускаю приложение, которое смешивает собственный код Java и C, используя JNA, и сталкиваюсь с воспроизводимой проблемой, когда сборщик мусора Java не может освободить ссылки на прямые выделения собственной памяти, что приводит к нехватке памяти кучи C.

Я уверен, что мое приложение C не является источником проблемы выделения, поскольку я передаю java.nio.ByteBuffer в свой код C, изменяю буфер и затем получаю доступ к результату в моей функции Java. У меня есть один malloc и один соответствующий free во время каждого вызова функции, но после многократного запуска кода на Java malloc в конечном итоге завершится ошибкой.

Вот несколько упрощенный набор кода, который демонстрирует проблему - реально я пытаюсь выделить около 16-32 МБ в куче C во время вызова функции .

Мой код Java делает что-то вроде:

public class MyClass{
    public void myfunction(){
        ByteBuffer foo = ByteBuffer.allocateDirect(1000000);
        MyDirectAccessLib.someOp(foo, 1000000);
        System.out.println(foo.get(0));
    }
}

public MyDirectAccessLib{
    static {
        Native.register("libsomelibrary");
    }
    public static native void someOp(ByteBuffer buf, int size);
}

Тогда мой код на C может выглядеть примерно так:

#include <stdio.h>
#include <stdlib.h>
void someOp(unsigned char* buf, int size){
    unsigned char *foo;
    foo = malloc(1000000);
    if(!foo){
        fprintf(stderr, "Failed to malloc 1000000 bytes of memory\n");
        return;
    }
    free(foo);

    buf[0] = 100;
}

Проблема в том, что после многократного вызова этой функции куча Java несколько стабильна (медленно растет), но функция C в конечном итоге не может выделить больше памяти. На высоком уровне я считаю, что это потому, что Java выделяет память для кучи C, но не очищает ByteBuffer, который указывает на эту память, потому что объект Java ByteBuffer относительно мал.

До сих пор я обнаружил, что запуск GC вручную в моей функции обеспечит необходимую очистку, но это кажется плохой идеей и плохим решением.

Как мне лучше справиться с этой проблемой, чтобы пространство ByteBuffer было соответствующим образом освобождено, а мое пространство кучи C контролировалось?

Правильно ли я понимаю проблему (есть что-то, что я запускаю неправильно)?

Редактировать : скорректированы размеры буфера, чтобы они были более отражающими мое реальное приложение, я выделяю для изображений приблизительно 3000x2000 ...

Ответы [ 5 ]

8 голосов
/ 21 ноября 2009

Вы на самом деле сталкиваетесь с известной ошибкой в ​​Java VM . Лучший обходной путь, указанный в отчете об ошибке:

  • "Параметр -XX: MaxDirectMemorySize = можно использовать для ограничения объема используемой прямой памяти. Попытка выделить прямую память, которая может привести к превышению этого предела, приводит к полному сборке мусора, чтобы вызвать обработку ссылок и выпуск буферы без ссылок. "

Другие возможные обходные пути включают в себя:

  • Вставьте случайные явные вызовы System.gc (), чтобы гарантировать, что прямые буферы будут освобождены.
  • Уменьшите размер молодого поколения, чтобы заставить более частые ГК.
  • Явно объединяет прямые буферы на уровне приложения.

Если вы действительно хотите полагаться на прямые байтовые буферы, я бы предложил использовать пул на уровне приложения. В зависимости от сложности вашего приложения вы можете даже просто кэшировать и повторно использовать один и тот же буфер (остерегайтесь нескольких потоков).

4 голосов
/ 16 ноября 2009

Я думаю, что вы правильно поставили диагноз: у вас никогда не заканчивалась куча Java, поэтому JVM не собирает мусор и сопоставленные буферы не освобождаются. Тот факт, что у вас нет проблем при запуске GC вручную, подтверждает это. Вы также можете включить подробное ведение журнала коллекции в качестве вторичного подтверждения.

Так что вы можете сделать? Ну, во-первых, я бы попытался сохранить начальный размер кучи JVM небольшим, используя аргумент командной строки -Xms. Это может вызвать проблемы, если ваша программа постоянно выделяет небольшие объемы памяти в куче Java, так как она будет чаще запускать GC.

Я бы также использовал инструмент pmap (или его эквивалент в Windows) для проверки карты виртуальной памяти. Возможно, вы фрагментируете кучу C, выделяя буферы переменного размера. Если это так, то вы увидите каждую большую виртуальную карту с промежутками между «анонными» блоками. И решение заключается в том, чтобы выделить блоки постоянного размера, которые больше, чем вам нужно.

1 голос
/ 04 июля 2012

Для освобождения памяти Buffer [ 1 ] можно использовать JNI .

Функция GetDirectBufferAddress(JNIEnv* env, jobject buf) [ 3 ] из JNI 6 API может использоваться для получения указателя на память для Buffer, а затем стандартная команда free(void *ptr) на указателе для освобождения памяти.

Вместо того, чтобы писать код, такой как C, для вызова указанной функции из Java, вы можете использовать JNA * Native.getDirectBufferPointer(Buffer) [ 6 ]

После этого остается только отказаться от всех ссылок на объект Buffer. После этого сборщик мусора в Java освободит экземпляр Buffer, как и любой другой объект, на который нет ссылок.

Обратите внимание, что прямое Buffer не обязательно отображает 1: 1 в выделенную область памяти. Например, API JNI имеет NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity) [ 7 ] . Таким образом, вы должны только освободить память из Buffer, чья область выделения памяти, как вы знаете, будет один к одному с собственной памятью.

Я также не знаю, можете ли вы освободить прямой Buffer, созданный Java ByteBuffer.allocateDirect(int) [ 8 ] по той же причине, что и выше. Это могут быть детали реализации JVM или платформы Java, независимо от того, используют ли они пул или выделяют 1: 1 памяти при раздаче новых прямых Buffer s.

Ниже приведен слегка измененный фрагмент из моей библиотеки, касающийся прямой обработки ByteBuffer [ 9 ] (используется JNA Native [ 10 ] и Pointer [ 11 ] классы):

/**
 * Allocate native memory and associate direct {@link ByteBuffer} with it.
 * 
 * @param bytes - How many bytes of memory to allocate for the buffer
 * @return The created {@link ByteBuffer}.
 */
public static ByteBuffer allocateByteBuffer(int bytes) {
        long lPtr = Native.malloc(bytes);
        if (lPtr == 0) throw new Error(
            "Failed to allocate direct byte buffer memory");
        return Native.getDirectByteBuffer(lPtr, bytes);
}

/**
 * Free native memory inside {@link Buffer}.
 * <p>
 * Use only buffers whose memory region you know to match one to one
 * with that of the underlying allocated memory region.
 * 
 * @param buffer - Buffer whose native memory is to be freed.
 * The class instance will remain. Don't use it anymore.
 */
public static void freeNativeBufferMemory(Buffer buffer) {
        buffer.clear();
        Pointer javaPointer = Native.getDirectBufferPointer(buffer);
        long lPtr = Pointer.nativeValue(javaPointer);
        Native.free(lPtr);
}
1 голос
/ 21 ноября 2009

Если у вас заканчивается куча памяти, GC запускается автоматически. Однако, если вам не хватает прямой памяти, GC не запускается (по крайней мере, на JVM от Sun), и вы просто получаете OutOfMemoryError, даже если GC освободит достаточно памяти. Я обнаружил, что в этой ситуации вы должны вручную запустить GC.

Лучшим решением может быть повторное использование одного и того же ByteBuffer, поэтому вам больше не нужно перераспределять ByteBuffers.

1 голос
/ 16 ноября 2009

Я подозреваю, что ваша проблема связана с использованием прямых байтовых буферов. Они могут быть размещены вне кучи Java.

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

Чтобы изолировать проблему, я бы переключился на буфер (Java), выделенный в куче (просто используйте метод allocate вместо allocateDirect. Если это устранит проблему с памятью, вы нашел виновника. Следующим вопросом было бы, имеет ли прямой байтовый буфер какое-либо преимущество в производительности. Если нет (и я предполагаю, что это не так), то вам не нужно беспокоиться о как правильно его почистить.

...