12

正如我们所知,Java 总是在创建时初始化数组。即new int[1000000]总是返回一个所有元素 = 0 的数组。我知道这对于对象数组是必须的,但对于原始数组(可能是布尔值除外),在大多数情况下,我们并不关心初始值。

有人知道避免这种初始化的方法吗?

4

4 回答 4

19

我做了一些调查。在 Java 中没有合法的方法来创建未初始化的数组。甚至 JNI NewXxxArray 也会创建初始化数组。所以不可能确切地知道数组归零的成本。尽管如此,我还是做了一些测量:

1) 1000 字节数组创建不同数组大小

        long t0 = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++) {
//          byte[] a1 = new byte[1];
            byte[] a1 = new byte[1000000];
        }
        System.out.println(System.currentTimeMillis() - t0);

在我的 PC 上,字节 [1] 的时间 < 1 毫秒,字节 [1000000] 的时间约为 500 毫秒。对我来说听起来很令人印象深刻。

2)我们在JDK中没有快速(原生)的方法来填充数组,Arrays.fill太慢了,所以让我们看看用原生System.arraycopy复制1,000,000大小的数组至少需要多少1000次

    byte[] a1 = new byte[1000000];
    byte[] a2 = new byte[1000000];
    for(int i = 0; i < 1000; i++) {
        System.arraycopy(a1, 0, a2, 0, 1000000);
    }

它是 700 毫秒。

它让我有理由相信 a) 创建长数组很昂贵 b) 由于无用的初始化,它似乎很昂贵。

3)让我们以 sun.misc.Unsafe http://www.javasourcecode.org/html/open-source/jdk/jdk-6u23/sun/misc/Unsafe.html为例。它可以防止外部使用,但不会太多

    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe)f.get(null);

这是内存分配测试的成本

    for(int i = 0; i < 1000; i++) {
        long m = u.allocateMemory(1000000);
    }

如果您还记得的话,它需要 < 1 毫秒,对于新字节 [1000000],它需要 500 毫秒。

4) Unsafe 没有直接的方法来处理数组。它需要知道类字段,但反射显示数组中没有字段。关于数组内部的信息不多,我猜它是 JVM / 平台特定的。尽管如此,它与任何其他 Java 对象一样,是标头 + 字段。在我的 PC/JVM 上,它看起来像

header - 8 bytes
int length - 4 bytes
long bufferAddress - 8 bytes

现在,使用 Unsafe,我将创建 byte[10],分配一个 10 字节的内存缓冲区并将其用作我的数组元素:

    byte[] a = new byte[10];
    System.out.println(Arrays.toString(a));
    long mem = unsafe.allocateMemory(10);
    unsafe.putLong(a, 12, mem);
    System.out.println(Arrays.toString(a));

它打印

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[8, 15, -114, 24, 0, 0, 0, 0, 0, 0]

您可以看到 thay 数组的数据未初始化。

现在我将更改我们的数组长度(尽管它仍然指向 10 字节内存)

    unsafe.putInt(a, 8, 1000000);
    System.out.println(a.length);

它显示 1000000。这只是为了证明这个想法有效。

现在进行性能测试。我将创建一个空字节数组 a1,分配一个 1000000 字节的缓冲区,将此缓冲区分配给 a1 并设置 a1.length = 10000000

    long t0 = System.currentTimeMillis();
    for(int i = 0; i < 1000; i++) {
        byte[] a1 = new byte[0];
        long mem1 = unsafe.allocateMemory(1000000);
        unsafe.putLong(a1, 12, mem);
        unsafe.putInt(a1, 8, 1000000);
    }
    System.out.println(System.currentTimeMillis() - t0);

它需要 10 毫秒。

5)C++中有malloc和alloc,malloc只是分配内存块,calloc也用零初始化。

cpp

...
JNIEXPORT void JNICALL Java_Test_malloc(JNIEnv *env, jobject obj, jint n) {
     malloc(n);
} 

爪哇

private native static void malloc(int n);

for (int i = 0; i < 500; i++) {
    malloc(1000000);
}

结果 malloc - 78 毫秒;calloc - 468 毫秒

结论

  1. 由于无用的元素归零,Java 数组的创建似乎很慢。
  2. 我们无法改变它,但 Oracle 可以。无需更改 JLS 中的任何内容,只需将本机方法添加到 java.lang.reflect.Array 之类的

    public static native xxx[] newUninitialziedXxxArray(int size);

对于所有原始数字类型(字节 - 双)和字符类型。它可以在整个 JDK 中使用,例如 java.util.Arrays

    public static int[] copyOf(int[] original, int newLength) {
        int[] copy = Array.newUninitializedIntArray(newLength);
        System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
        ...

或 java.lang.String

   public String concat(String str) {
        ...   
        char[] buf = Array.newUninitializedCharArray(count + otherLen);
        getChars(0, count, buf, 0);
        ...
于 2012-12-09T06:32:01.690 回答
4

我将把它移到一个答案,因为它可能应该是。

java中的“数组”不是你想象的那样。它不仅仅是指向堆栈或堆上一块连续内存的指针。

Java 中的数组是一个对象,就像其他所有东西(除了原语)一样,位于堆上。当您调用时,new int[100000]您正在创建一个新对象,就像其他所有对象一样,它会被初始化,等等。

JLS 提供了有关此的所有特定信息:

http://docs.oracle.com/javase/specs/jls/se5.0/html/arrays.html

所以不行。你不能避免“初始化”一个数组。这不是Java的工作方式。根本不存在未初始化的堆内存。许多人称其为“功能”,因为它会阻止您访问未初始化的内存。

于 2012-12-08T18:31:40.247 回答
1

Java 9 实际上开始暴露这个 viajdk.internal.misc.Unsafe.allocateUninitializedArray方法。它实际上需要 JDK.Unsupported 模块声明

于 2018-11-15T22:25:11.013 回答
0

我可以想象 new int[n] 的 O(n) 成本可能是某些数据结构或算法的负担。

在 Java 中为大小为 n 的原始数组分摊 O(1) 内存分配成本的一种方法是使用对象池或其他策略回收分配的数组。对于下一次分配,回收的数组可以被认为是“未初始化的”。

于 2019-04-03T17:38:55.533 回答