Подтвердить что ты не робот

Java: эффективная память ByteArrayOutputStream

У меня есть 40-мегабайтный файл на диске, и мне нужно "сопоставить" его в памяти с помощью байтового массива.

Сначала я подумал, что написать файл в ByteArrayOutputStream будет лучшим способом, но я считаю, что в какой-то момент во время операции копирования требуется около 160 МБ кучного пространства.

Знает ли кто-нибудь лучший способ сделать это, не используя в три раза размер файла ОЗУ?

Обновление: Спасибо за ваши ответы. Я заметил, что я мог уменьшить потребление памяти, немного сказав, что начальный размер ByteArrayOutputStream будет немного больше исходного размера файла (с использованием точного размера с моим перераспределением сил кода, чтобы проверить, почему).

Там еще одно место высокой памяти: когда я получаю байт [] обратно с ByteArrayOutputStream.toByteArray. Взглянув на его исходный код, я вижу, что он клонирует массив:

public synchronized byte toByteArray()[] {
    return Arrays.copyOf(buf, count);
}

Я думаю, что могу просто расширить ByteArrayOutputStream и переписать этот метод, чтобы вернуть исходный массив напрямую. Есть ли потенциальная опасность здесь, если поток и массив байтов не будут использоваться более одного раза?

4b9b3361

Ответ 1

MappedByteBuffer может быть тем, что вы ищете.

Я удивлен, что для чтения файла в памяти требуется так много оперативной памяти. Вы построили ByteArrayOutputStream с соответствующей емкостью? Если вы этого не сделали, поток может выделять новый массив байтов, когда он приближается к концу 40 МБ, что означает, что у вас будет, например, полный буфер 39 МБ и новый буфер в два раза больше. Если поток имеет соответствующую пропускную способность, не будет никакого перераспределения (быстрее) и нет потерянной памяти.

Ответ 2

ByteArrayOutputStream должно быть в порядке, если вы укажете соответствующий размер в конструкторе. Он по-прежнему будет создавать копию, когда вы вызываете toByteArray, но это только временное. Вы действительно помните, что память ненадолго поднимается?

В качестве альтернативы, если вы уже знаете, какой размер начать, вы можете просто создать массив байтов и повторно читать из FileInputStream в этот буфер, пока не получите все данные.

Ответ 3

Если вы действительно хотите отобразить файл в память, то FileChannel является подходящим механизмом.

Если все, что вы хотите сделать, - это прочитать файл в простой byte[] (и не нуждаться в изменениях в этом массиве, которые будут отображаться обратно в файл), а затем просто читать в формате byte[] соответствующего размера нормальный FileInputStream должен быть достаточным.

Guava имеет Files.toByteArray(), который делает все это для вас.

Ответ 4

Если у вас 40 МБ данных, я не вижу причин, по которым для создания байта [] потребуется более 40 МБ. Я предполагаю, что вы используете растущий ByteArrayOutputStream, который создает копию byte [] при завершении.

Вы можете попробовать старое чтение файла за один раз.

File file = 
DataInputStream is = new DataInputStream(FileInputStream(file));
byte[] bytes = new byte[(int) file.length()];
is.readFully(bytes);
is.close();

Использование MappedByteBuffer более эффективно и позволяет избежать копирования данных (или с использованием большой кучи), если вы можете использовать ByteBuffer напрямую, однако если вам нужно использовать байт [], вряд ли это поможет.

Ответ 5

... но я обнаружил, что в некоторый момент во время операции копирования требуется около 160 Мбайт пространства кучи.

Я нахожу это чрезвычайно удивительным... насколько я сомневаюсь, что вы правильно измеряете использование кучи.

Предположим, что ваш код выглядит примерно так:

BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("somefile"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();  /* no hint !! */

int b;
while ((b = bis.read()) != -1) {
    baos.write((byte) b);
}
byte[] stuff = baos.toByteArray();

Теперь способ, которым ByteArrayOutputStream управляет своим буфером, состоит в том, чтобы выделить начальный размер и (по крайней мере) удвоить буфер, когда он заполняет его. Таким образом, в худшем случае baos может использовать буфер до 80 МБ для хранения файла 40 Мб.

Последний шаг выделяет новый массив точно baos.size() байтов для хранения содержимого буфера. Это 40Mb. Таким образом, максимальный объем памяти, который фактически используется, должен составлять 120 МБ.

Итак, где эти дополнительные 40 Мб используются? Я предполагаю, что это не так, и что вы на самом деле сообщаете общий размер кучи, а не объем памяти, который занят доступными объектами.


Итак, каково решение?

  • Вы можете использовать буфер с отображением памяти.

  • Вы можете указать подсказку размера, когда вы выделяете ByteArrayOutputStream; например.

     ByteArrayOutputStream baos = ByteArrayOutputStream(file.size());
    
  • Вы можете полностью обойтись без ByteArrayOutputStream и читать непосредственно в массив байтов.

     byte[] buffer = new byte[file.size()];
     FileInputStream fis = new FileInputStream(file);
     int nosRead = fis.read(buffer);
     /* check that nosRead == buffer.length and repeat if necessary */
    

Оба варианта 1 и 2 должны иметь пиковое использование памяти 40 Мб при чтении файла 40 Мб; то есть нет пробелов.


Было бы полезно, если бы вы разместили свой код и описали свою методологию измерения использования памяти.


Я думаю, что могу просто расширить ByteArrayOutputStream и переписать этот метод, чтобы вернуть исходный массив напрямую. Есть ли потенциальная опасность здесь, если поток и массив байтов не будут использоваться более одного раза?

Потенциальная опасность заключается в том, что ваши предположения неверны или становятся неверными из-за того, что кто-то другой невольно модифицирует ваш код...

Ответ 6

Для объяснения поведения роста буфера ByteArrayOutputStream, пожалуйста, прочитайте этот ответ.

В ответ на ваш вопрос безопасно расширять ByteArrayOutputStream. В вашей ситуации, вероятно, лучше переопределить методы записи таким образом, чтобы максимальное дополнительное распределение было ограничено, скажем, до 16 МБ. Вы не должны переопределять toByteArray, чтобы открыть защищенный элемент buf []. Это связано с тем, что поток не является буфером; Поток - это буфер, который имеет указатель положения и защиту границ. Таким образом, опасно получить доступ и потенциально манипулировать буфером вне класса.

Ответ 7

Google Guava ByteSource кажется хорошим выбором для буферизации в памяти. В отличие от реализаций, таких как ByteArrayOutputStream или ByteArrayList (из библиотеки Colt), он не объединяет данные в огромный массив байтов, а сохраняет каждый фрагмент отдельно. Пример:

List<ByteSource> result = new ArrayList<>();
try (InputStream source = httpRequest.getInputStream()) {
    byte[] cbuf = new byte[CHUNK_SIZE];
    while (true) {
        int read = source.read(cbuf);
        if (read == -1) {
            break;
        } else {
            result.add(ByteSource.wrap(Arrays.copyOf(cbuf, read)));
        }
    }
}
ByteSource body = ByteSource.concat(result);

ByteSource можно прочитать как InputStream в любое время позже:

InputStream data = body.openBufferedStream();

Ответ 8

Я думаю, что могу просто расширить ByteArrayOutputStream и переписать этот метод, чтобы вернуть исходный массив напрямую. Есть ли потенциальная опасность здесь, если поток и массив байтов не будут использоваться более одного раза?

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

/** Subclasses ByteArrayOutputStream to give access to the internal raw buffer. */
public class ByteArrayOutputStream2 extends java.io.ByteArrayOutputStream {
    public ByteArrayOutputStream2() { super(); }
    public ByteArrayOutputStream2(int size) { super(size); }

    /** Returns the internal buffer of this ByteArrayOutputStream, without copying. */
    public synchronized byte[] buf() {
        return this.buf;
    }
}

Альтернативным, но хакерским способом получения буфера из любого ByteArrayOutputStream является использование того факта, что метод writeTo(OutputStream) передает буфер непосредственно на предоставленный OutputStream:

/**
 * Returns the internal raw buffer of a ByteArrayOutputStream, without copying.
 */
public static byte[] getBuffer(ByteArrayOutputStream bout) {
    final byte[][] result = new byte[1][];
    try {
        bout.writeTo(new OutputStream() {
            @Override
            public void write(byte[] buf, int offset, int length) {
                result[0] = buf;
            }

            @Override
            public void write(int b) {}
        });
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    return result[0];
}

(Это работает, но я не уверен, полезно ли это, учитывая, что подкласс ByteArrayOutputStream проще.)

Однако из остальной части вашего вопроса это похоже на то, что все, что вам нужно, - это просто byte[] полного содержимого файла. Начиная с Java 7, самым простым и быстрым способом является вызов Files.readAllBytes. В Java 6 и ниже вы можете использовать DataInputStream.readFully, как в ответить Питеру Лори. В любом случае вы получите массив, выделенный один раз с правильным размером, без повторного перераспределения ByteArrayOutputStream.