众所周知,Java在创建数组时总是会进行初始化。即new int[1000000]
始终返回一个所有元素都等于0的数组。我了解这对于对象数组来说是必要的,但对于原始数组(除了可能是布尔类型)而言,在大多数情况下我们并不关心初始值。
有人知道如何避免这种初始化吗?
我进行了一些调查。在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);
在我的电脑上,byte[1]只需要<1毫秒,而byte [1000000]大约需要500毫秒。听起来很不错。
2)我们在JDK中没有一个快速(本地)填充数组的方法,Arrays.fill 太慢了,所以让我们至少看一下使用本地System.arraycopy复制1000个1000000大小的数组需要多长时间。
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);
}
如果您还记得,创建new byte [1000000]只需要不到1毫秒,但它需要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]
您可以看到数组的数据未被初始化。
现在我将更改数组的长度(尽管它仍指向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会将其初始化为0。
cpp
...
JNIEXPORT void JNICALL Java_Test_malloc(JNIEnv *env, jobject obj, jint n) {
malloc(n);
}
Java
private native static void malloc(int n);
for (int i = 0; i < 500; i++) {
malloc(1000000);
}
结果 malloc - 78 毫秒; calloc - 468 毫秒
结论
我们不能改变它,但是Oracle可以。 不需要在JLS中更改任何内容,只需向java.lang.reflect.Array添加本机方法,如
public static native xxx[] newUninitialziedXxxArray(int size);
对于所有的原始数值类型(byte- double)和char类型都可以使用它。 它可以在整个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);
...
currentTimeMillis
测量纳秒事件,将allocate
的返回值(即块地址)放入数组中,并得出结论它是“未初始化”的... - Marko Topolnik我将把这个问题转移到答案中,因为它可能应该是这样。
在Java中,“Array”不是你想象的那样。它不仅仅是指向栈或堆上连续内存块的指针。
在Java中,数组是一个对象,就像其他所有东西(除了原始类型)一样,它位于堆上。当您调用new int [100000]
时,您正在创建一个新对象,就像每个其他对象一样,并进行初始化等操作。
JLS提供了有关此的所有具体信息:
http://docs.oracle.com/javase/specs/jls/se5.0/html/arrays.html
所以,不。你不能避免“初始化”数组。这就是Java的工作方式。没有未初始化的堆内存;许多人称其为“特性”,因为它防止您访问未初始化的内存。Java 9实际上通过jdk.internal.misc.Unsafe.allocateUninitializedArray
方法开始暴露这一点。它实际上需要JDK.Unsupported模块声明。
我可以想象,在某些数据结构或算法中,新建int[n]的O(n)成本可能是一种负担。
在Java中,实现一个大小为n的原始数组的摊销O(1)内存分配成本的方法是使用对象池或其他策略回收已分配的数组。回收的数组可以被视为下一次分配时的“未初始化”状态。
java.util.Arrays.fill(char[] a, char val)
将其填充为 0,并测量时间,以发现初始化时间与将数组填充为有意义数据所需的时间相比微不足道。 - Alexei Kaigorodovsun.misc.Unsafe
创建自己的内存缓冲区的方法,这样就不会有效地"memset(0)"。如果您有疯狂的性能要求,请阅读更多相关信息。 - kevinarpe