Check out the VM for Factor. It is really well-written:
https://github.com/factor/factor/tree/master/vm It's based on HotSpot so you could check out its code too.
In short, the answer is to think of everything as an object. A pointer is an object, a block of compiled code is an object, a return address is an object, an execution context is an object and so on. All these objects are related to each other in one huge object graph that the VM keeps track of. For example, if one code object (such as a function) calls another, then the VM views each address in each assembler CALL instruction as one pointer, pointing from one function to the other. So if foo calls bar and bar is recompiled, a new code block which we'll call bar' would be inserted at the end of the code heap and then each CALL instruction in foo would be updated to point to bar' instead.
This can get quite complicated if another execution context is running baz which was called from bar. How can we ensure that it returns to bar' instead of bar? We would have to go through the stack of the execution context and update all return addresses stored in it so that they point to bar' instead of bar.
Of course all this has to be done as efficiently as possible so VM implementors use huge amounts of bit fiddling tricks which obscures what the code is doing...