Ответ 1
Ответ Евгения объясняет причину того, почему вы наблюдаете такое увеличение потребления памяти для большого количества массивов. Вопрос в заголовке "Как эффективно хранить массивы небольших байт в Java?", Может ответить на это: совсем нет. 1
Однако, вероятно, есть способы достижения ваших целей. Как обычно, "лучшее" решение здесь будет зависеть от того, как эти данные будут использоваться. Очень прагматичный подход: Определить interface
для вашей структуры данных.
В простейшем случае этот интерфейс может быть просто
interface ByteArray2D
{
int getNumRows();
int getNumColumns();
byte get(int r, int c);
void set(int r, int c, byte b);
}
Предлагает базовую абстракцию "массива двумерных байтов". В зависимости от случая применения может быть полезно предложить дополнительные методы здесь. Шаблоны, которые могут быть использованы здесь, часто имеют отношение к библиотекам Matrix, которые обрабатывают "2D-матрицы" (обычно из значений float
), и они часто предлагают такие методы:
interface Matrix {
Vector getRow(int row);
Vector getColumn(int column);
...
}
Однако, когда основной целью здесь является обработка набора массивов byte[]
, могут быть достаточными методы для доступа к каждому массиву (то есть к каждой строке 2D-массива):
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) при вызове одного из методов для доступа к определенной строке/столбцу 2D-массива.
Ниже вы найдете 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
Показывая, что
-
реализация
SimpleByteArray2D
, основанная на простом массиве 2Dbyte[][]
, требует больше памяти при увеличении количества строк (даже если общий размер массива остается постоянным) -
потребление памяти
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/С++ структура, которая наиболее близко напоминает "2D-массив Java", представляет собой массив выделенных вручную указателей:
char** array;
array = new (char*)[numRows];
array[0] = new char[numCols];
...
В этом случае у вас также есть накладные расходы, которые пропорциональны количеству строк, а именно одному (обычно 4 байтовому) указателю для каждой строки.