32-bit OOP pointer is just multiplied by 8, because all objects have address divisible by 8. 4 GB * 8 = 32 GB.
Example of generated code from some random web page that happened to be there:
http://cr.openjdk.java.net/~shade/8050147/rsp-minus-8.perfas...
[0x7f189918ae07:0x7f189918ae54] in org.openjdk.VolatileBarrierBench::testWith
# [sp+0x30] (sp of caller)
0x00007f189918ade0: mov 0x8(%rsi),%r10d
0x00007f189918ade4: shl $0x3,%r10
That "shl $0x3,%r10" multiplies the pointer in
register r10 by 8 (2^3). So looks like JVM computes the base pointer to an object to a register (r10 in this case) and uses relative addressing from it.
Here's example of using x86 indirect addressing mode to multiply the pointer by 8:
http://shipilev.net/blog/2014/safe-public-construction/stead...
0x00007fc7b4971644: mov 0xc(%r12,%r11,8),%r10d ;*getfield instance
In this case effective address for the load from heap is r12 + r11 * 8. Register r12 probably represents heap base pointer or similar in this case - I'm not sure.
16-bit would be significantly slower, but for entirely other reasons. Also, you could have at most 512 kB heap with 8 byte address alignment...
The issue with 64-bit pointers is that Java (and JVM) is a very pointer happy language, so the cost is significantly more than for pretty much any other language I can think of. Memory usage and more cache misses is the issue.