不合理利用List导致的高内存占用

371 阅读1分钟

背景

在项目中,考虑到了传输数据的占用大小与解析性能。在与前端的数据传输格式上并没有用传统的JSON类型,而是自定义了一种的数据格式并用byte数组传输。

public class Data {
    private List<Byte> data = new LinkedList<>();

    public void append(byte b) {
        data.add(b);
    }
}

在这段代码中,具体的数据存储在了data这个List中,外部解析完数据后将单个byte接进来。

在看到这行代码时有没有疑问?

private List<Byte> data = new LinkedList<>();

当初考虑List没有用数组的原因是,数组的初始化需要知道数据的长度,而List可以动态扩容。当初想偷个懒,因为没有写预先统计数据大小的逻辑。

接下来看具体的使用逻辑

public class Main {
    public statc void main(String[] args) {
        // 70M
        int dataSize = 1024 * 1024 * 70;
        Data myData = new Data();
        // 模拟插入数据
        for (int i = 0; i < dataSize; i++) {
            myData.append((byte) (i % 256));
        }
    }
}

大家可以猜一下70M的数据全部加入链表后占用的内存会有多大。

接下来我们实际运行一下,然后查看内存占用。

从图中可以看到,该List间接占用的内存有1.6G!但它实际上想表达的数据才70M。与用byte数组相比接近涨了20倍!!! image.png

问题原因

原因其实很简单,直接从上面的堆栈分析的结果很容易看到。如果用byte数组表示的话,数据中的一个元素就只占用一字节大小,而用LinkedList来表示的话,用一个Node表示一个字节,而一个Node对象就占用到了24字节,就是意味着内存占用翻了至少24倍!属实是有为了这一滴醋,包了一锅的饺子的味道了。

问题修复

改为ArrayList

还记得以前面试时背的八股文吗? ArrayList与LinkedList的区别? LinkedList的实现是通过链表实现的,因此需要一个Node对象来维护链表的关系。而ArrayList是通过数组实现的,不需要额外的对象维护数据间的关系,所以占用少。更详细的内容可以参考ArrayList与LinkedList性能比较

image.png 上图是换为ArrayList后的内存占用大小,虽然内存占用少了很多,但内存占用大小还是byte数组的6倍左右。如果在不知道数据大小与不想直接写动态扩容的逻辑且数据量不大的情况下可以使用ArrayList。

推荐:直接使用byte数组

在遇到用到基本数据类型的数组的情况下,如果能够知道数据的大小,应当尽量用数组。因为这样是效率最高且内存占用最小的。如果不知道数据大小的情况下通过增加自定义扩容的逻辑来动态扩容数组,扩容数组的逻辑可以借鉴(抄)ArrayList。

image.png

这里最重要的逻辑就是Arrays.copyof()这个函数,这个函数会复制原数组并返回一个扩容后的数组。

最新的Data如下

public class Data {
    private final static int DEFAULT_SIZE = 1024 * 1024;
    private final static byte[] EMPTY = new byte[0];

    private byte[] data = EMPTY;
    private int current = 0;

    public byte[] getData() {
        if (data.length > current + 1) {
            byte[] newData = new byte[current];
            System.arraycopy(this.data, 0, newData, 0, current);
            this.data = newData;
        }
        return data;
    }

    public void append(byte b) {
        if (data == EMPTY) {
            data = new byte[DEFAULT_SIZE];
        }
        if (current + 1 >= data.length) {
            grow();
        }
        data[current++] = b;
    }

    private void grow() {
        int oldSize = data.length;
        int newSize = oldSize + (oldSize >> 1);
        this.data = Arrays.copyOf(data, newSize);
    }
}