There are a variety of VMs, including VMs that use “registers”. However, the point of the VM is that it abstracts over physical machines so the VM will likely not limit the number of registers! Such registers are then essentially equivalent to local variables.
- If the VM uses an interpreted execution model, both stack-based VMs and register-based VMs will typically access an array.
- If the VM uses a compiled execution model, both stack based and register based VMs will allocate physical registers (if the compilation target uses registers), and spill the remainder to memory. Optimal register allocation is a hard problem either way.
Stack based VMs are slightly easier to implement and have significant historical precedents to the point that there is a whole class of languages which are stack based (concatenative languages). If the VM is implemented in a high level language there can be some practical difficulties if stack entries can have different sizes (e.g. byte vs. word), and because stack entries have unknown types. E.g. in JVM instructions are typed and the VM can statically verify during bytecode loading that the bytecode will not corrupt the stack.
Register based VMs can have a slightly easier time with optimizations if the intermediate language is given in Static Single Assignment form (SSA). Each logical register is assigned at exactly one instruction and is read-only afterwards. This makes it easy to analyse data flows. Of course this prevents a naive mapping from logical to physical registers, but that is no disadvantage compared to stack based VMs. However, most interpreters or JIT compilers are not very concerned with optimizability.
The most well known register based VM is LLVM, which uses SSA. This VM is mostly used as an abstract instruction set (intermediate representation) in ahead-of-time compilers such as clang or rustc.
Register based VMs also occasionally pop up for dynamic language’s runtimes, but are more niche here. Parrot and MoarVM (both connected to the Perl6 language) come to mind.