Java has a couple of different options for dealing with objects but is able to manage the full set of pointers and know that (for example) code doesn't try to access memory+1 for looping etc.
Under 4G worth of size pointers are simply zero extended, so that the higher order bits are just zero and the lower order bits stores in 32 bits.
Above 4G but below 32G it switches to "compressed oops" mode. This right-shifts the address by 3 bits and ensures/mandates that all objects are aligned to 8 bits (2^3). When converting back again to a memory address it multiplies by 8. This effectively multiplies the top address space by a factor of 8 and so you can store an object in the same 32 bits but can be wider.
It's possible to shift the alignment (-XX:ObjectAlignmentInBytes=16) which will then right shift the address by 4 instead giving you 64Gb memory with 32 bit addressing.
Although it's possible to go higher the advantages tend to scale out; effectively you end up padding the object space more and more so simple objects take up larger and larger slots.
The bottom line is: by controlling where pointers are used, Java can use a different addressing scheme by unpacking and repacking object pointers on the fly. And for some operations (equality testing) it doesn't even need to unpack the pointers in the first place.