【问题标题】:How to efficiently store small byte arrays in Java?如何在 Java 中高效地存储小字节数组?
【发布时间】:2018-01-31 11:11:57
【问题描述】:

small 字节数组是指长度从 10 到 30 的字节数组。

存储是指将它们存储在RAM中,而不是序列化和持久化到文件系统。

系统macOS 10.12.6,Oracle jdk1.8.0_141 64bit,JVM args -Xmx1g

例子: new byte[200 * 1024 * 1024] 的预期行为是 ≈200mb 的堆空间

public static final int TARGET_SIZE = 200 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
    byte[] arr = new byte[TARGET_SIZE];
    System.gc();
    System.out.println("Array size: " + arr.length);
    System.out.println("HeapSize: " + Runtime.getRuntime().totalMemory());
    Thread.sleep(60000);
}

但是对于较小的数组,数学并不是那么简单

public static final int TARGET_SIZE = 200 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
    final int oneArraySize = 20;
    final int numberOfArrays = TARGET_SIZE / oneArraySize;
    byte[][] arrays = new byte[numberOfArrays][];
    for (int i = 0; i < numberOfArrays; i++) {
        arrays[i] = new byte[oneArraySize];
    }
    System.gc();
    System.out.println("Arrays size: " + arrays.length);
    System.out.println("HeapSize: " + Runtime.getRuntime().totalMemory());
    Thread.sleep(60000);
}

甚至更糟

问题是

这些开销从何而来? 如何有效地存储和使用小字节数组(数据块)?

更新 1

new byte[200*1024*1024][1] 它吃

基本数学表示new byte[1] 权重 24 个字节。

更新 2

根据What is the memory consumption of an object in Java? Java 中对象的最小大小为 16 字节。从我之前的 "measurements" 24 个字节 -4 个字节用于 int 长度 -1 个实际字节的数据 = 3 个字节的一些 其他垃圾 填充。

【问题讨论】:

  • 这些开销从何而来? 数组对象本身也占用内存。这不仅仅是他们的内容。即使是空数组也会占用内存。
  • array是一个对象,比Typex[ N ]多,有head,type declaration,vtable,block的GC信息,和所有对象一样。通过艰苦的专业工作,例如迁移到“堆外”,垃圾收集器不起作用,一切都是完全手动的(从操作系统获取数百万字节并做任何想做的事)。许多缓存系统都有堆外缓冲区。数值系统?我不知道,理论上可能
  • 每个数组都有the public final field length, which contains the number of components of the array. length may be positive or zero。每个数组的最小开销为 4 字节(加上java.lang.Object 继承的任何开销)。对于 10 个字节的数组,这至少是 40% 的惩罚(每个数组)。您究竟想达到什么目标?
  • 如果您仍然对标题中的问题的答案感兴趣:将其隐藏在界面后面。 interface Data { byte get(int x, int y); void set(int x, int y, byte b)。然后,您可以将所有内容存储在一个数组中。如果更方便,您还可以以ByteBuffer 的形式返回这个大数组的“切片”(使用ByteBuffer#slice 方法)。
  • @Eugene 添加。在这种情况下,我总是要克制自己,详细说明每种方法的所有实现选项和优缺点……但是,关键点可能是(仅?)在使用(隐藏)时可以避免开销用于存储的一维数组。

标签: java arrays memory memory-efficient


【解决方案1】:

好的,所以如果我理解正确(如果不是请询问 - 将尝试回答),这里有几件事。首先是您需要合适的测量工具,而JOL 是我唯一信任的工具。

让我们从简单的开始:

byte[] two = new byte[1];
System.out.println(GraphLayout.parseInstance(one).toFootprint()); 

这将显示24 bytes12 用于 markclass 字 - 或对象标头 + 4 字节填充),1 byte 用于实际值,7 bytes for padding(内存对齐 8 字节) .

考虑到这一点,这应该是一个可预测的输出:

byte[] eight = new byte[8];
System.out.println(GraphLayout.parseInstance(eight).toFootprint()); // 24 bytes

byte[] nine = new byte[9];
System.out.println(GraphLayout.parseInstance(nine).toFootprint()); // 32 bytes

现在让我们转到二维数组:

byte[][] ninenine = new byte[9][9];    
System.out.println(GraphLayout.parseInstance(ninenine).toFootprint()); // 344 bytes

System.out.println(ClassLayout.parseInstance(ninenine).toPrintable());

由于java没有true二维数组;每个嵌套数组本身都是一个具有标题和内容的对象 (byte[])。因此,单个byte[9] 具有32 bytes12 标头+4 填充)和16 bytes 用于内容(9 bytes 用于实际 内容+7 bytes 填充)。

ninenine 对象总共有 56 字节:16 标头 + 36 用于保留对九个对象的引用 + 4 bytes 用于填充。


在此处查看生成的示例:

byte[][] left = new byte[10000][10];
System.out.println(GraphLayout.parseInstance(left).toFootprint()); // 360016 bytes

byte[][] right = new byte[10][10000];
System.out.println(GraphLayout.parseInstance(right).toFootprint()); // 100216 bytes

增加了 260%;因此,只需更改为其他方式,您就可以节省大量空间。

但更深层次的问题是,Java 中的每个对象都有这些标头,没有标头 对象还没有。它们可能会出现并称为Value Types。可能是在实现的时候——原语数组至少不会有这种开销。

【讨论】:

  • 附注:“值类型”是一个非常古老的提议,可能会以一种或另一种形式出现。特别是,在 Project Panama 中,已经对使用这些类型的“Vector API”进行了广泛的讨论。虽然很遗憾,我最近没有设法深入了解讨论和建议,但这可能是相关的(请参阅mail.openjdk.java.net/pipermail/panama-dev/2017-July/… 周围的消息 - 其中有很多),至少当 i>假设这些小字节数组最终应该是“某种向量”。
【解决方案2】:

answer by Eugene 解释了您观察到大量数组的内存消耗如此增加的原因。标题中的问题,“如何在 Java 中有效地存储小字节数组?”,然后可以回答:根本没有。 1

但是,可能有一些方法可以实现您的目标。像往常一样,这里的“最佳”解决方案将取决于这些数据将如何被使用。一个非常实用的方法是:为您的数据结构定义一个interface

在最简单的情况下,这个接口可能只是

interface ByteArray2D 
{
    int getNumRows();
    int getNumColumns();
    byte get(int r, int c);
    void set(int r, int c, byte b);
}

提供“二维字节数组”的基本抽象。根据应用案例,在此处提供其他方法可能会有所帮助。此处可以使用的模式通常与处理“二维矩阵”(通常为 float 值)的 矩阵库 相关,并且它们通常提供如下方法:

interface Matrix {
    Vector getRow(int row);
    Vector getColumn(int column);
    ...
}

但是,当这里的主要目的是处理一组byte[] 数组时,访问每个数组(即二维数组的每一行)的方法就足够了:

ByteBuffer getRow(int row);

给定这个接口,创建不同的实现很简单。例如,您可以创建一个简单的实现,只在内部存储一个 2D byte[][] 数组:

class SimpleByteArray2D implements ByteArray2D 
{
    private final byte array[][];
    ...
}

或者,您可以创建一个在内部存储 1D byte[] 数组或类似的 ByteBuffer 的实现:

class CompactByteArray2D implements ByteArray2D
{
    private final ByteBuffer buffer;
    ...
}

然后,此实现只需在调用访问二维数组的特定行/列的方法之一时计算 (1D) 索引。

您将在下面找到一个MCVE,其中显示了此接口和两个实现、接口的基本用法,并使用 JOL 进行了内存占用分析。

这个程序的输出是:

For 10 rows and 1000 columns:
Total size for SimpleByteArray2D : 10240
Total size for CompactByteArray2D: 10088

For 100 rows and 100 columns:
Total size for SimpleByteArray2D : 12440
Total size for CompactByteArray2D: 10088

For 1000 rows and 10 columns:
Total size for SimpleByteArray2D : 36040
Total size for CompactByteArray2D: 10088

显示

  • 基于简单二维byte[][] 数组的SimpleByteArray2D 实现在行数增加时需要更多内存(即使数组的总大小保持不变)

  • CompactByteArray2D 的内存消耗独立与数组的结构

整个程序:

package stackoverflow;

import java.nio.ByteBuffer;

import org.openjdk.jol.info.GraphLayout;

public class EfficientByteArrayStorage
{
    public static void main(String[] args)
    {
        showExampleUsage();
        anaylyzeMemoryFootprint();
    }

    private static void anaylyzeMemoryFootprint()
    {
        testMemoryFootprint(10, 1000);
        testMemoryFootprint(100, 100);
        testMemoryFootprint(1000, 10);
    }

    private static void testMemoryFootprint(int rows, int cols)
    {
        System.out.println("For " + rows + " rows and " + cols + " columns:");

        ByteArray2D b0 = new SimpleByteArray2D(rows, cols);
        GraphLayout g0 = GraphLayout.parseInstance(b0);
        System.out.println("Total size for SimpleByteArray2D : " + g0.totalSize());
        //System.out.println(g0.toFootprint());

        ByteArray2D b1 = new CompactByteArray2D(rows, cols);
        GraphLayout g1 = GraphLayout.parseInstance(b1);
        System.out.println("Total size for CompactByteArray2D: " + g1.totalSize());
        //System.out.println(g1.toFootprint());
    }

    // Shows an example of how to use the different implementations
    private static void showExampleUsage()
    {
        System.out.println("Using a SimpleByteArray2D");
        ByteArray2D b0 = new SimpleByteArray2D(10, 10);
        exampleUsage(b0);

        System.out.println("Using a CompactByteArray2D");
        ByteArray2D b1 = new CompactByteArray2D(10, 10);
        exampleUsage(b1);
    }

    private static void exampleUsage(ByteArray2D byteArray2D)
    {
        // Reading elements of the array
        System.out.println(byteArray2D.get(2, 4));

        // Writing elements of the array
        byteArray2D.set(2, 4, (byte)123);
        System.out.println(byteArray2D.get(2, 4));

        // Bulk access to rows
        ByteBuffer row = byteArray2D.getRow(2);
        for (int c = 0; c < row.capacity(); c++)
        {
            System.out.println(row.get(c));
        }

        // (Commented out for this MCVE: Writing one row to a file)
        /*/
        try (FileChannel fileChannel = 
            new FileOutputStream(new File("example.dat")).getChannel())
        {
            fileChannel.write(byteArray2D.getRow(2));
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        //*/
    }

}


interface ByteArray2D 
{
    int getNumRows();
    int getNumColumns();
    byte get(int r, int c);
    void set(int r, int c, byte b);

    // Bulk access to rows, for convenience and efficiency
    ByteBuffer getRow(int row);
}

class SimpleByteArray2D implements ByteArray2D 
{
    private final int rows;
    private final int cols;
    private final byte array[][];

    public SimpleByteArray2D(int rows, int cols)
    {
        this.rows = rows;
        this.cols = cols;
        this.array = new byte[rows][cols];
    }

    @Override
    public int getNumRows()
    {
        return rows;
    }

    @Override
    public int getNumColumns()
    {
        return cols;
    }

    @Override
    public byte get(int r, int c)
    {
        return array[r][c];
    }

    @Override
    public void set(int r, int c, byte b)
    {
        array[r][c] = b;
    }

    @Override
    public ByteBuffer getRow(int row)
    {
        return ByteBuffer.wrap(array[row]);
    }
}

class CompactByteArray2D implements ByteArray2D
{
    private final int rows;
    private final int cols;
    private final ByteBuffer buffer;

    public CompactByteArray2D(int rows, int cols)
    {
        this.rows = rows;
        this.cols = cols;
        this.buffer = ByteBuffer.allocate(rows * cols);
    }

    @Override
    public int getNumRows()
    {
        return rows;
    }

    @Override
    public int getNumColumns()
    {
        return cols;
    }

    @Override
    public byte get(int r, int c)
    {
        return buffer.get(r * cols + c);
    }

    @Override
    public void set(int r, int c, byte b)
    {
        buffer.put(r * cols + c, b);
    }

    @Override
    public ByteBuffer getRow(int row)
    {
        ByteBuffer r = buffer.slice();
        r.position(row * cols);
        r.limit(row * cols + cols);
        return r.slice();
    }
}

同样,这主要是作为一个草图,展示一种可能的方法。接口的细节将取决于预期的应用程序模式。


1旁注:

内存开销的问题在其他语言中是类似的。例如,在 C/C++ 中,最类似于“2D Java 数组”的结构是手动分配的指针数组:

char** array;
array = new (char*)[numRows];
array[0] = new char[numCols];
...

在这种情况下,您还有与行数成正比的开销 - 即,每行有一个(通常为 4 字节)指针。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-04-09
    • 2017-12-11
    • 2012-10-31
    • 2014-03-03
    • 2014-01-31
    • 2019-07-07
    • 1970-01-01
    • 2018-07-18
    相关资源
    最近更新 更多