用于动态字节存储的碎片阵列的缺点

时间:2013-10-04 06:51:44

标签: java arrays

默认的ByteArrayOutputStream似乎是一个相当浪费的实现,我想知道是否有任何具体原因。首先,它在后端保留1个固定阵列。如果它已满,它会创建一个新数组并将旧数组复制到其中(更多内存+更多开销)。然后,如果你执行toByteArray(),它实际上会再次复制数组。

字节缓冲区很好但是也固定了大小,它们只在单个阵列上提供了一些,仅此而已。

我想知道创建一个使用一个或多个支持数组的类(或者它是否已存在,请指向我)是否有趣。它不是每次都要重复扩展数组,而是添加一个新的后备数组。要阅读您可以轻松创建一个类似输入流的界面,同时您可以公开像输出流这样的界面来编写

关于这样的事情是否已经存在的任何反馈,如果没有:为什么?它有一些我不会看到的缺点吗?

4 个答案:

答案 0 :(得分:2)

这实际上是一个好主意,特别是对于大数据。

在堆上分配大型数组时,您可以快速遇到内存问题,因为它们需要分配连续的可用内存。当我们经常分配大小为10-50MB的字节数组并且遇到OutOfMemoryException时,我们曾经遇到过这样的情况,不是因为可用的内存太少(我们通常有90%,或900MB免费),但是因为由于堆碎片,没有一个连续的内存块可用于此阵列。

我们最终创建了一个Blob类,它在内部将数据存储为链式(List)较小的数组的块。这些数组具有固定的大小(对于快速查找至关重要,因此您可以快速计算给定索引所涉及的数组和偏移量),并为此Blob创建InputStreamOutputStream类。后来我们将它扩展为可以从磁盘上交换。

  • 下行?没有,除了一点点简单的编程工作。
  • 好处?在内存中高效存储大数据,不再存在问题 堆碎片。

我只能鼓励你试一试!

答案 1 :(得分:0)

C ++标准库既有一个向量类(如Java ArrayList),也有一个deque类(另一个像类一样的List)。后者提供有效的前置和有效的附加。我见过的实现维护了固定长度数组的列表。所以,有点像你感兴趣的情况。所以,这当然是可能的。

缺点是代码的复杂性大大增加。我想可以改变JRE中的实现来做你所建议的,使用toByteArray方法从片段中收集数据。但是这样做的优先级非常低,因为简单的实现速度非常快。执行IO的任何代码都应该假设读取和写入操作很慢,这可能会阻塞。 ByteArrayOutputStream非常快,因为它在内存操作中执行而不是在真正的外部IO中执行。复制这些字节数组可能比外部IO快得多。当前实现的缺点是在用于大输出流时创建大型垃圾数组。但是这个类的用例是针对小流;如果要临时存储大输出流的字节,则应使用临时文件。因此,提案的复杂性可能在实践中没有多大帮助

答案 2 :(得分:0)

看起来你已经知道了好处。与单个缓冲区相比,缓冲区列表的缺点包括:

  • 如果缓冲区大小固定,则需要O(n)内存分配来写入n个字节,ByteArrayOutputStream使得O(log n)因为缓冲区呈指数级增长
  • 实现更复杂:你必须跟踪活动缓冲区,可能需要在写入过程中切换缓冲区(取决于设计)
  • 切换缓冲区是读取时的缓存未命中

如果对您的应用程序有意义,您可以自由编写这样的数据结构

答案 3 :(得分:0)

由于似乎没有真正的实现,我已经快速编写了一个初始实现来测试速度:

public class Buffer {

    private int size;

    private int writeIndex, writeOffset,
        readIndex, readOffset;

    private List<byte[]> backingArrays = new ArrayList<byte[]>();

    public Buffer() {
        this(10240);
    }

    public Buffer(int size) {
        this.size = size;
    }

    public int read(byte [] bytes) {
        return read(bytes, 0, bytes.length);
    }

    public int read(byte [] bytes, int offset, int length) {
        int read = 0;
        while(length > 0) {
            byte [] input = getInput();
            // no more data
            if (input == null) {
                if (read == 0)
                    return -1;
                else
                    return read;
            }
            int readLength = Math.min(length, (readIndex == writeIndex ? writeOffset : size) - readOffset);
            System.arraycopy(input, readOffset, bytes, offset, readLength);
            length -= readLength;
            offset += readLength;
            readOffset += readLength;
            read += readLength;
        }
        return read;
    }

    public void write(byte [] bytes) {
        write(bytes, 0, bytes.length);
    }

    public void write(byte [] bytes, int offset, int length) {
        while (length > 0) {
            byte [] output = getOutput();
            int writeLength = Math.min(length, output.length - writeOffset);
            System.arraycopy(bytes, offset, output, writeOffset, writeLength); 
            length -= writeLength;
            offset += writeLength;
            writeOffset += writeLength;
        }
    }

    private byte[] getOutput() {
        // if we have filled an array, move to the next one
        if (writeOffset >= size) {
            writeIndex++;
            writeOffset = 0;
        }
        // create it if it doesn't exist yet
        if (backingArrays.size() <= writeIndex)
            backingArrays.add(new byte[size]);

        return backingArrays.get(writeIndex);
    }

    private byte [] getInput() {
        // nothing written yet
        if (backingArrays.size() == 0)
            return null;

        if (readOffset >= size) {
            readIndex++;
            readOffset = 0;
        }
        // can not read past where it is written
        if (readIndex > writeIndex || (readIndex == writeIndex && readOffset >= writeOffset))
            return null;
        else
            return backingArrays.get(readIndex);
    }

    public long size() {
        return (long) size * (long) writeIndex + writeOffset;
    }
}

我正在通过复制36 meg文件来测试它。很多当然取决于文件交互,但一般来说,读取速度比bytearrayinputstream略快快40%(徘徊在5-20%左右)

我很快把它放在一起,所以如果你发现任何错误,请告诉我。

修改

添加了一项功能,默认情况下,为gc

发布已读取的数组