Java: эффективная память ByteArrayOutputStream - PullRequest
15 голосов
/ 31 августа 2011

У меня есть 40-мегабайтный файл на диске, и мне нужно «отобразить» его в памяти, используя байтовый массив.

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

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

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

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

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

Ответы [ 9 ]

13 голосов
/ 31 августа 2011

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

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

10 голосов
/ 31 августа 2011

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

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

5 голосов
/ 31 августа 2011

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

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

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

3 голосов
/ 05 декабря 2012

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

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

2 голосов
/ 10 ноября 2014

Я думаю, я мог бы просто расширить 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;
    }
}

Альтернативный, но хакерский способ получения буфера из any 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.

2 голосов
/ 25 сентября 2014

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();
2 голосов
/ 31 августа 2011

... но я считаю, что в какой-то момент во время операции копирования это занимает около 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() байтов для хранения содержимого буфера. Это 40 Мб. Таким образом, максимальный объем используемой памяти должен составлять 120 МБ.

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


Так в чем же решение?

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

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

     ByteArrayOutputStream baos = ByteArrayOutputStream(file.size());
    
  3. Вы можете полностью отказаться от 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 и переписать этот метод, чтобы вернуть исходный массив напрямую. Есть ли здесь потенциальная опасность, учитывая, что поток и байтовый массив не будут использоваться более одного раза?

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

2 голосов
/ 31 августа 2011

Если у вас 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 напрямую, однако, если вам нужно использовать байт [], вряд ли это сильно поможет.

0 голосов
/ 27 мая 2018

... пришел сюда с тем же наблюдением при чтении файла объемом 1 ГБ: Oracle ByteArrayOutputStream имеет ленивое управление памятью. Byte-Array индексируется с помощью int и так или иначе ограничивается 2 ГБ. Вне зависимости от сторонних разработчиков это может оказаться полезным:

static public byte[] getBinFileContent(String aFile) 
{
    try
    {
        final int bufLen = 32768;
        final long fs = new File(aFile).length();
        final long maxInt = ((long) 1 << 31) - 1;
        if (fs > maxInt)
        {
            System.err.println("file size out of range");
            return null;
        }
        final byte[] res = new byte[(int) fs];
        final byte[] buffer = new byte[bufLen];
        final InputStream is = new FileInputStream(aFile);
        int n;
        int pos = 0;
        while ((n = is.read(buffer)) > 0)
        {
            System.arraycopy(buffer, 0, res, pos, n);
            pos += n;
        }
        is.close();
        return res;
    }
    catch (final IOException e)
    {
        e.printStackTrace();
        return null;
    }
    catch (final OutOfMemoryError e)
    {
        e.printStackTrace();
        return null;
    }
}
...