JNA / ByteBuffer не освобождается и вызывает нехватку памяти в куче C

Позвольте мне начать с того, что мое понимание того, как 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 ...


person Mark Elliot    schedule 16.11.2009    source источник


Ответы (5)


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

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

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

person kdgregory    schedule 16.11.2009

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

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

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

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

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

person Gregory Pakosz    schedule 21.11.2009
comment
Я использую -XX:MaxDirectMemorySize=128m, но все равно получаю OutOfMemoryError, когда создаю и отбрасываю слишком много Memory экземпляров, не вызывая System.gc(). С его помощью пропадают ошибки. - person Mark Jeronimus; 20.10.2015

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

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

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

person erickson    schedule 16.11.2009
comment
AFAIK единственный способ передать ByteBuffer в собственный код через JNA - это _2 _... ошибка, которую я видел при использовании allocate вместо allocateDirect. Шаблон использования здесь на самом деле заключается в передаче буферов изображений, размер которых составляет около 3000 * 2000 байт. - person Mark Elliot; 16.11.2009
comment
Я тоже так предположил, а затем внимательно изучил документы JNA (jna.dev.java .net / # mapping), и похоже, что вы можете использовать обычный массив Java при условии, что вы не удерживаете этот массив за пределами вызова собственной функции. Однако это может привести к накладным расходам на копирование. - person kdgregory; 16.11.2009
comment
Другими словами, пропустите использование объекта nio.ByteBuffer и просто передайте _2 _... стоит попробовать. - person Mark Elliot; 16.11.2009
comment
Да, вы также можете использовать wrap для предоставления доступа к byte[] интерфейсу JNA, позволяя при этом Java-коду работать с ByteBuffer. - person erickson; 16.11.2009

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

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

person Peter Lawrey    schedule 21.11.2009

Чтобы освободить прямые Buffer's [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 в выделенную область памяти. Например, в JNI API есть NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity) [7]. Таким образом, вы должны освобождать память только для Buffer, чья область выделения памяти, как вы знаете, находится один к одному с собственной памятью.

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

Ниже следует слегка измененный фрагмент из моей библиотеки, касающийся прямого 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);
}
person Gima    schedule 03.07.2012