如何在 Java 中有效地存储小字节数组?
How to efficiently store small byte arrays in Java?
小 字节数组我的意思是长度从 10 到 30 的字节数组。
通过 store 我的意思是将它们存储 在 RAM 中,而不是序列化和持久化到文件系统。
System macOS 10.12.6, Oracle jdk1.8.0_141 64bit, JVM args -Xmx1g
示例:
new byte[200 * 1024 * 1024]
的预期行为是堆的 ≈200mb space
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 字节 填充。
好的,所以如果我理解正确(请询问是否不正确 - 会尝试回答),这里有几件事。首先是您需要正确的测量工具,JOL 是我唯一信任的工具。
让我们从简单开始:
byte[] two = new byte[1];
System.out.println(GraphLayout.parseInstance(one).toFootprint());
这将显示 24 bytes
(12
用于 mark
和 class
字 - 或 Object headers + 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二维数组;每个嵌套数组本身就是一个具有 headers 和内容的 Object (byte[]
)。因此,单个 byte[9]
具有 32 bytes
(12
headers + 4
填充)和 16 bytes
用于内容(9 bytes
用于 实际内容+7 bytes
填充)。
ninenine
object 总共 56
字节:16
headers + 36
用于保留对九个 objects + 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%;因此,只需更改 其他方式 ,您就可以节省很多 space.
但更深层次的问题是 Java 中的每个 Object 都有那些 headers,没有 headerless objects 还。它们可能会出现并被称为 Value Types。可能是在实施时——基元数组至少不会有这种开销。
answer by Eugene 解释了为什么您观察到大量数组的内存消耗增加的原因。标题中的问题 "How to efficiently store small byte arrays in Java?" 可能会回答:一点也不。 1
但是,可能有一些方法可以实现您的目标。与往常一样,这里的 "best" 解决方案将取决于这些数据将如何 使用 。一个非常实用的方法是:为您的数据结构定义一个 interface
。
在最简单的情况下,这个接口可以是
interface ByteArray2D
{
int getNumRows();
int getNumColumns();
byte get(int r, int c);
void set(int r, int c, byte b);
}
提供“二维字节数组”的基本抽象。根据应用案例,在此处提供其他方法可能会有所帮助。可以在这里使用的模式通常与 矩阵库 相关,它们处理“2D 矩阵”(通常具有 float
值),并且它们通常提供如下方法:
interface Matrix {
Vector getRow(int row);
Vector getColumn(int column);
...
}
但是,当这里的主要目的是处理一组byte[]
数组时,访问每个数组的方法(即二维数组的每一行) 就足够了:
ByteBuffer getRow(int row);
鉴于此接口,创建不同的实现很简单。例如,您可以创建一个仅在内部存储二维 byte[][]
数组的简单实现:
class SimpleByteArray2D implements ByteArray2D
{
private final byte array[][];
...
}
或者,您可以创建一个实现来存储 1D byte[]
数组,或者类似地,在内部存储一个 ByteBuffer
:
class CompactByteArray2D implements ByteArray2D
{
private final ByteBuffer buffer;
...
}
此实现只需在调用一种方法来访问二维数组的某个 row/column 时计算 (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
显示
基于简单 2D byte[][]
数组的 SimpleByteArray2D
实现在行数增加时需要更多内存(即使数组的总大小保持不变)
CompactByteArray2D
的内存消耗与结构的数组无关
整个程序:
package Whosebug;
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 字节)指针。
小 字节数组我的意思是长度从 10 到 30 的字节数组。
通过 store 我的意思是将它们存储 在 RAM 中,而不是序列化和持久化到文件系统。
System macOS 10.12.6, Oracle jdk1.8.0_141 64bit, JVM args -Xmx1g
示例:
new byte[200 * 1024 * 1024]
的预期行为是堆的 ≈200mb space
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 字节 填充。
好的,所以如果我理解正确(请询问是否不正确 - 会尝试回答),这里有几件事。首先是您需要正确的测量工具,JOL 是我唯一信任的工具。
让我们从简单开始:
byte[] two = new byte[1];
System.out.println(GraphLayout.parseInstance(one).toFootprint());
这将显示 24 bytes
(12
用于 mark
和 class
字 - 或 Object headers + 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二维数组;每个嵌套数组本身就是一个具有 headers 和内容的 Object (byte[]
)。因此,单个 byte[9]
具有 32 bytes
(12
headers + 4
填充)和 16 bytes
用于内容(9 bytes
用于 实际内容+7 bytes
填充)。
ninenine
object 总共 56
字节:16
headers + 36
用于保留对九个 objects + 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%;因此,只需更改 其他方式 ,您就可以节省很多 space.
但更深层次的问题是 Java 中的每个 Object 都有那些 headers,没有 headerless objects 还。它们可能会出现并被称为 Value Types。可能是在实施时——基元数组至少不会有这种开销。
answer by Eugene 解释了为什么您观察到大量数组的内存消耗增加的原因。标题中的问题 "How to efficiently store small byte arrays in Java?" 可能会回答:一点也不。 1
但是,可能有一些方法可以实现您的目标。与往常一样,这里的 "best" 解决方案将取决于这些数据将如何 使用 。一个非常实用的方法是:为您的数据结构定义一个 interface
。
在最简单的情况下,这个接口可以是
interface ByteArray2D
{
int getNumRows();
int getNumColumns();
byte get(int r, int c);
void set(int r, int c, byte b);
}
提供“二维字节数组”的基本抽象。根据应用案例,在此处提供其他方法可能会有所帮助。可以在这里使用的模式通常与 矩阵库 相关,它们处理“2D 矩阵”(通常具有 float
值),并且它们通常提供如下方法:
interface Matrix {
Vector getRow(int row);
Vector getColumn(int column);
...
}
但是,当这里的主要目的是处理一组byte[]
数组时,访问每个数组的方法(即二维数组的每一行) 就足够了:
ByteBuffer getRow(int row);
鉴于此接口,创建不同的实现很简单。例如,您可以创建一个仅在内部存储二维 byte[][]
数组的简单实现:
class SimpleByteArray2D implements ByteArray2D
{
private final byte array[][];
...
}
或者,您可以创建一个实现来存储 1D byte[]
数组,或者类似地,在内部存储一个 ByteBuffer
:
class CompactByteArray2D implements ByteArray2D
{
private final ByteBuffer buffer;
...
}
此实现只需在调用一种方法来访问二维数组的某个 row/column 时计算 (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
显示
基于简单 2D
byte[][]
数组的SimpleByteArray2D
实现在行数增加时需要更多内存(即使数组的总大小保持不变)CompactByteArray2D
的内存消耗与结构的数组无关
整个程序:
package Whosebug;
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 字节)指针。