Besides, as mentioned java's allocations are much closer to something like using an arena in a low-level language, then a "slow" malloc. It uses thread-local allocation buffers, where you have a large buffer with a pointer pointing to the start of the free region. Allocation is just a pointer bump, not even needing synchronization since it is per a single thread. As it gets full, the GC moves out still alive objects in the background and resets the buffer.
This is pretty much the most efficient one can be after per-object type arenas and simply not allocating.
Allocations are really really cheap in Java by the way, so I don't get how 500 allocations would even be an issue.
Goose jim = make_a_goose_somehow(); // Java, so jim is on the Heap, no way around it
When you make that variable in Rust... let jim : Goose = make_a_goose_somehow(); // Rust, jim is on the stack
Now, if we make a bunch of geese, maybe in a loop, and we put them into our growable array type... ArrayList<Goose> geese = new ArrayList<Goose>();
// ... some loop eventually
Goose a_goose = somehow_get_this_goose(); // That's an allocation
geese.add(a_goose);
But in Rust... let geese: Vec<Goose> = Vec::new();
// ... some loop eventually
let a_goose : Goose = somehow_get_this_goose(); // But this is not
geese.push(a_goose);