@唐生 大大的回答就是正解了,请给他的回答点赞。关键点就在于“zeroing”——GC堆上申请的空间要被清零,在Linux这种commit on-demand的环境中最迟在“往内存写入0”的动作会触发其对应的内存被实际commit,没那么多内存可用的话直接就要被系统OOM killer干掉了。
我就来跑个题而已。把题主给的Go的例子换成Java,稍微改写一下方便演示。
(注:当然,Go的slice跟Java的数组并不直接对等;硬要说的话它某些方面像Java数组(例如长度不是类型的一部分),而另一些方面像Java的ArrayList(例如带有一层间接,可以支持扩容等)。正因为Go的slice可以扩容,slice这个类型上并没有保证len是不可变的,所以不像Java的数组的length有保证不可变而可以做进一步优化。
不过就题主的例子而言,未来Go的编译器优化能力提升之后,还是有机会达到下面演示的效果的。)
public class DemoEA {
public static final long LEN_IN_GO_DEMO = 10_000_000_000L;
public static final int LEN_IN_THIS_DEMO = (int) LEN_IN_GO_DEMO / 10;
static {
System.out.println();
}
public static void main(String[] args) {
// for (int i = 0; i < 100; i++) { long[] c = new long[LEN_IN_THIS_DEMO]; // ~7.4GB System.out.println(c.length);
// } }
}
这里把题主的例子里slice长度给缩小到1/10,是因为Java语言规范规定数组长度一定要是int类型的,而题主例子里的长度超范围了。不过这不重要。循环也给注释掉了因为也不重要。
用GraalVM 0.22版(使用Graal作为JIT编译器的HotSpot VM)来跑这个测试,并且强制让main()一开始就被JIT编译好,会看到main()对应的机器码是这样的:
DemoEA.main (null) [0x000000010bc00da0, 0x000000010bc00e10] 112 bytes
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x000000011563fb58} 'main' '([Ljava/lang/String;)V' in 'DemoEA'
# parm0: rsi:rsi = '[Ljava/lang/String;'
# [sp+0x20] (sp of caller)
0x000000010bc00da0: mov %eax,-0x14000(%rsp)
0x000000010bc00da7: sub $0x18,%rsp
0x000000010bc00dab: mov %rbp,0x10(%rsp)
0x000000010bc00db0: movabs $0x1184420a0,%rsi ;*getstatic out {reexecute=0 rethrow=0 return_oop=0}
; - DemoEA::main@5 (line 12)
; {oop(a 'java/lang/Class' = 'java/lang/System')}
0x000000010bc00dba: mov 0xa8(%rsi),%rsi ; OopMap{rsi=Oop off=33}
;*ldc {reexecute=1 rethrow=0 return_oop=0}
; - DemoEA::main@0 (line 11)
0x000000010bc00dc1: test %eax,(%rsi)
0x000000010bc00dc3: mov $0x86796cc,%edx ;*invokevirtual println {reexecute=0 rethrow=0 return_oop=0}
; - DemoEA::main@10 (line 12)
0x000000010bc00dc8: nopl 0x0(%rax)
0x000000010bc00dcf: callq 0x000000010bba9020 ; OopMap{off=52}
;*invokevirtual println {reexecute=0 rethrow=0 return_oop=0}
; - DemoEA::main@10 (line 12)
; {optimized virtual_call}
0x000000010bc00dd4: nop ;*invokevirtual println {reexecute=0 rethrow=0 return_oop=0}
; - DemoEA::main@10 (line 12)
0x000000010bc00dd5: mov 0x10(%rsp),%rbp
0x000000010bc00dda: add $0x18,%rsp
0x000000010bc00dde: test %eax,-0x1cc0dde(%rip) # 0x0000000109f40006
; {poll_return}
0x000000010bc00de4: vzeroupper
0x000000010bc00de7: retq
用Java来表示就是:
public static void main(String[] args) {
System.out.println(1_000_000_000);
}
那个大数组的分配直接被消除了,所以它只要在语言规范允许的范围内有多大并不会影响实验结果——不会由于大数组分配而触发GC,是否放在循环里也无所谓。我这里实验把循环注释掉主要是为了让输出的机器码短一点,不然Graal会默认循环展开很多次,读着麻烦。
* 实验的命令是:
$ graalvm-0.22/bin/java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-TieredCompilation -Xcomp -XX:CompileCommand=compileonly,DemoEA,main -XX:CompileCommand=dontinline,java/*,* -XX:+PrintCompilation -XX:-UseCompressedOops DemoEA
好玩不? ^_^
注:完全相同的实验在原装HotSpot VM上用C2作为JIT编译器则达不到这个效果。C2会通过常量传播把 System.out.println(c.length) 变成 System.out.println(1_000_000_000),但那个无用的数组分配还是给留着了…所以这里我换了个JIT编译器来演示。嘻嘻,反正选择多。