根据JVM的内存粒度确定数组的最佳大小

Dur*_*dal 6 java arrays memory-management micro-optimization

为(例如)集合创建支持数组时,您并不真正关心所创建数组的确切大小,它只需要至少与您计算的一样大.

但是由于内存分配和VM的数组头,在某些情况下可以创建一个更大的阵列而不消耗更多的内存 - 对于Oracle 32位VM(至少这是互联网上的几个来源声称),内存粒度为8(意味着任何内存分配向上舍入到下一个8字节边界),并且数组头开销为12个字节.

这意味着在分配Object [2]时,它应该消耗20个字节(12 + 2*4),但由于粒度,它实际上需要24个字节.可以为相同的内存成本创建一个Object [3],这意味着集合必须稍后调整其后备阵列的大小.相同的原理可以应用于primitve数组,例如用于I/O缓冲区的byte [],字符串生成器中的char []等.

虽然这种优化不会产生明显的效果,但在最极端的情况下,调用静态方法来"优化"数组大小并不会太麻烦.

问题是,JDK中没有这种"圆形阵列大小直至内存粒度".并且自己编写这样的方法需要确定VM的一些关键参数:内存粒度,数组头开销以及最终每种类型的大小(主要是引用的问题,因为它们的大小可能随架构和VM选项而变化).

那么有没有一种方法来确定这些参数,或通过其他方式实现所需的"向上舍入"?

Cel*_*ggs 2

有趣的想法。我认为确定这一点的更便携的方法是实际测量使用情况。示例程序:

public class FindMemoryUsage {
    public static void main(String[] args) {
        for (int i=0; i<50; i+=2) {
            long actual = getActualUsageForN(i);
            System.out.println(i + " = " + actual);
            long theoretical = getTheoreticalUsageForN(i);
            if (theoretical != actual) {
                throw new RuntimeException("Uh oh! Mismatch!");
            }
        }
    }

    private static long getTheoreticalUsageForN(long count) {
        long optimal = (Unsafe.ARRAY_BYTE_BASE_OFFSET + Unsafe.ARRAY_BYTE_INDEX_SCALE * count);
        return ((optimal - 1) & ~7) + 8;
    }

    private static long getActualUsageForN(int count) {
        System.gc();
        byte[][] arrays = new byte[3000000][];
        long begin = usedMemory();
        for (int i=0; i<arrays.length; i++) {
            arrays[i] = new byte[count];
        }
        long end = usedMemory();
        return Math.round((end - begin) / (double) arrays.length);
    }

    private static long usedMemory() {
        return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
    }
}
Run Code Online (Sandbox Code Playgroud)

该程序为您提供以下信息:

0 = 16
2 = 16
4 = 16
6 = 24
8 = 24
10 = 24
12 = 24
14 = 32
16 = 32
18 = 32
20 = 32
22 = 40
24 = 40
26 = 40
28 = 40
30 = 48
32 = 48
34 = 48
36 = 48
38 = 56
40 = 56
42 = 56
44 = 56
46 = 64
48 = 64
Run Code Online (Sandbox Code Playgroud)

该数据来自实际计算的使用量和基于sun.misc.Unsafe的常数和 8 字节舍入的理论使用量。这意味着您可以像您建议的那样使用这些常量进行“四舍五入”:

private static int roundSizeUp(int from) {
    long size = (Unsafe.ARRAY_BYTE_BASE_OFFSET + Unsafe.ARRAY_BYTE_INDEX_SCALE * from);
    long actual = ((size - 1) & ~7) + 8;
    return (int) (actual - Unsafe.ARRAY_BYTE_BASE_OFFSET) / Unsafe.ARRAY_BYTE_INDEX_SCALE;
}
Run Code Online (Sandbox Code Playgroud)

getActualUsageForN这是特定于虚拟机的代码,但如果您需要更多的可移植性,您可能会根据策略找到如何执行此操作的方法。

请注意,这不是生产质量的代码:您需要仔细考虑溢出并将引用更改Unsafe为实际适用于您正在使用的数组类型的常量。