At lower optimisation levels there's a register allocated by the JVM to refer back to the bytecode, which makes things easy. In principle they could change that with a JVM revision - but in practice they don't, so it's an easy cheat.
We have some ability to walk data structures and the re-compute the program's behaviour by other means, which I probably shouldn't get into here. I think we could fall back on that more-or-less completely if we couldn't retrieve the bytecode pointer directly.
The fact the JVM introduces Safe Points to help it transition between optimisation levels is quite helpful!
Our original intention was to always fork a copy of the JVM back in time to handle Java debug protocol requests but that turned out to be painful and, thankfully, also unnecessary.