The job of a VM is to execute arbitrary code. (Put aside thoughts of the JIT for a second, we are just talking the pure VM). How do you expect to compile arbitrary programs down to bytecode for your VM? You have to have an instruction set. That intruction set has to work on something. If you are talking about directly operating variables from a high level language, well sure that is possible, but it isn't really a VM. It is just an interpreter. An interpreter can be symbolic (ie. it can work on names), and it can be high level. It can operate on AST nodes, or on pure text. But a VM generally is lower level, emulates a simple CPU, and the bytecode itself is a form of intermediate language. So you can distribute your programs in bytecode form which gives you some level of protection (though in Java and .NET world not as much as it could were the VM a bit lower level).
The downside to high level intepreters is they do a lot of symbolic work every time you execute the program. The strength of a VM comes when combined with the compiler. Take C# for example. Sure you can write a program to interpreter C# at a high level (I did this for the Parrot VM years ago), but the execution is much more efficient if you do some of the work ahead of time like compile variables down to registers or stack operands. Then your program becomes much denser than ASTs, and is again, conveniently represented as dense bytecode.
You define virtual operations that are simpler, or closer to real instructions to be compiled on the fly to real native executable by the JITter, also known as Just In Time Compilation. The simpler, more low level the ops are, the more efficient the JITter implementation is.
Also, in non-JIT mode (pure VM operation) if you compile the symbolic accesses down to registers or stack operands, you can improve the performance of your VM by making sure the core register area is cacheable. If your variables are all over the heap, and you opt to operate on them directly, you lose cache efficiency. With a register VM we might design it with fixed registers, or unlimited virtual registers, but in any case, an allocator will map them to fixed registers that are packed into a contiguous segment of RAM that we hope will fit into less L1 cache lines and gain performance for spatial locality.
But a high level symbolic VM that operates on variables directly is possible. Here are a couple of alternatives.
VM A - Symbolic:
add "count", 1
VM B - Register (memory transfer):
move $R1, "count"
The benefit of VM B is the opcodes are very close to or easily translatable to real live processors, because most processors don't allow operations on memory variables and require you to load the variable into a register or onto a stack. The JIT will have much less to do. With VM A, the
add operation is really a fat, aggregate operation which has to be compiled into individual native ops.
For a simple comparison to the two approaches study Perl5 and study C#/.NET bytecode. While it is possible to compile Perl5 to a bytecode, it isn't the default form of it. Perl really does more symbolic interpretation of an in-memory AST. That is why it was a major architectural change when we wrote Parrot for Perl6, though I'm not convinced it was the right decision now for that language.