Let’s see
public class Test {
public static void main(String[] args) {
for(int i = 0; i < Integer.MAX_VALUE; i++) {
if(test(i) != i + 0x123)
throw new AssertionError();
}
}
static int test(int someArg) {
int[] arr = new int[1000];
arr[42] = someArg;
return arr[42] + 0x123;
}
}
jdk-17\bin\javac -d %tmp% src\Test.java
jdk-17\bin\java -XX:CompileCommand=print,Test.test -cp %tmp% Test
…
============================= C2-compiled nmethod ==============================
----------------------------------- Assembly -----------------------------------
…
--------------------------------------------------------------------------------
[Verified Entry Point]
# {method} {0x0000020061400350} 'test' '(I)I' in 'Test'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
0x000002004e6e2f00: sub $0x18,%rsp
0x000002004e6e2f07: mov %rbp,0x10(%rsp)
0x000002004e6e2f0c: mov %edx,%eax
0x000002004e6e2f0e: add $0x123,%eax
0x000002004e6e2f14: add $0x10,%rsp
0x000002004e6e2f18: pop %rbp
0x000002004e6e2f19: cmp 0x340(%r15),%rsp ; {poll_return}
0x000002004e6e2f20: ja 0x000002004e6e2f27
0x000002004e6e2f26: ret
0x000002004e6e2f27: movabs $0x2004e6e2f19,%r10 ; {internal_word}
0x000002004e6e2f31: mov %r10,0x358(%r15)
0x000002004e6e2f38: jmp 0x0000020046c63400 ; {runtime_call SafepointBlob}
0x000002004e6e2f3d: hlt
0x000002004e6e2f3e: hlt
0x000002004e6e2f3f: hlt
[Exception Handler]
0x000002004e6e2f40: jmp 0x0000020046d05480 ; {no_reloc}
[Deopt Handler Code]
0x000002004e6e2f45: call 0x000002004e6e2f4a
0x000002004e6e2f4a: subq $0x5,(%rsp)
0x000002004e6e2f4f: jmp 0x0000020046c626a0 ; {runtime_call DeoptimizationBlob}
0x000002004e6e2f54: hlt
0x000002004e6e2f55: hlt
0x000002004e6e2f56: hlt
0x000002004e6e2f57: hlt
--------------------------------------------------------------------------------
As we can see, aside from the instructions dealing with the method invocation itself (i.e. stack manipulation), the C2 generated code for the test method consists of the two instructions, mov %edx,%eax and add $0x123,%eax. No array allocation, no array zeroing.
This is, of course, the most extreme scenario. But it demonstrates, that there is no requirement to do what has been written literally, as long as the resulting behavior is compatible.
Besides that, the term “allocation” is ambiguous. It implies changing some data structure to record that a particular region of memory is not available for other purposes. But there are differences in who shares this structure and hence, the same point of view regarding what is allocated.
Small allocations are usually done in a TLAB which is dedicated to a single thread. So allocating a small array in a TLAB only makes a difference to that thread. From the other threads’ point of view, the memory of that TLAB wasn’t available anyway. But even when a new TLAB is allocated from the heap memory shared by all threads, it’s memory that is considered already reserved to the JVM process from the operating system’s point of view. Only when the JVM expands or shrinks the entire heap memory, it will affect what the operating system reserves to the JVM (which is what may have an impact on other processes).
But even when memory is allocated from the operating system, it doesn’t have to be backed by physical memory. The operating system may postpone this operation to the point where the first actual write operation happens. Up to that point, the memory may still be considered all-zero-filled, which is the typical default, and the system does not need actual memory for an all-zero-filled region.