There are a lot of similarities between both implementations (and in my opinion: yes, they’re both “virtual machines”).
For one thing, they’re both stack-based VM’s, with no notion of “registers” like we’re used to seeing in a modern CPU like the x86 or PowerPC. The evaluation of all expressions ((1 + 1) / 2) is performed by pushing operands onto the “stack” and then popping those operands off the stack whenever an instruction (add, divide, etc) needs to consume those operands. Each instruction pushes its results back onto the stack.
It’s a convenient way to implement a virtual machine, because pretty much every CPU in the world has a stack, but the number of registers is often different (and some registers are special-purpose, and each instruction expects its operands in different registers, etc).
So, if you’re going to model an abstract machine, a purely stack-based model is a pretty good way to go.
Of course, real machines don’t operate that way. So the JIT compiler is responsible for performing “enregistration” of bytecode operations, essentially scheduling the actual CPU registers to contain operands and results whenever possible.
So, I think that’s one of the biggest commonalities between the CLR and the JVM.
As for differences…
One interesting difference between the two implementations is that the CLR includes instructions for creating generic types, and then for applying parametric specializations to those types. So, at runtime, the CLR considers a List<int> to be a completely different type from a List<String>.
Under the covers, it uses the same MSIL for all reference-type specializations (so a List<String> uses the same implementation as a List<Object>, with different type-casts at the API boundaries), but each value-type uses its own unique implementation (List<int> generates completely different code from List<double>).
In Java, generic types are a purely a compiler trick. The JVM has no notion of which classes have type-arguments, and it’s unable to perform parametric specializations at runtime.
From a practical perspective, that means you can’t overload Java methods on generic types. You can’t have two different methods, with the same name, differing only on whether they accept a List<String> or a List<Date>. Of course, since the CLR knows about parametric types, it has no problem handling methods overloaded on generic type specializations.
On a day-to-day basis, that’s the difference that I notice most between the CLR and the
JVM.
Other important differences include:
-
The CLR has closures (implemented as C# delegates). The JVM does support closures only since Java 8.
-
The CLR has coroutines (implemented with the C# ‘yield’ keyword). The JVM does not.
-
The CLR allows user code to define new value types (structs), whereas the JVM provides a fixed collection of value types (byte, short, int, long, float, double, char, boolean) and only allows users to define new reference-types (classes).
-
The CLR provides support for declaring and manipulating pointers. This is especially interesting because both the JVM and the CLR employ strict generational compacting garbage collector implementations as their memory-management strategy. Under ordinary circumstances, a strict compacting GC has a really hard time with pointers, because when you move a value from one memory location to another, all of the pointers (and pointers to pointers) become invalid. But the CLR provides a “pinning” mechanism so that developers can declare a block of code within which the CLR is not allowed to move certain pointers. It’s very convenient.
-
The largest unit of code in the JVM is either a ‘package’ as evidenced by the ‘protected’ keyword or arguably a JAR (i.e. Java ARchive) as evidenced by being able to specifiy a jar in the classpath and have it treated like a folder of code. In the CLR, classes are aggregated into ‘assemblies’, and the CLR provides logic for reasoning about and manipulating assemblies (which are loaded into “AppDomains”, providing sub-application-level sandboxes for memory allocation and code execution).
-
The CLR bytecode format (composed of MSIL instructions and metadata) has fewer instruction types than the JVM. In the JVM, every unique operation (add two int values, add two float values, etc) has its own unique instruction. In the CLR, all of the MSIL instructions are polymorphic (add two values) and the JIT compiler is responsible for determining the types of the operands and creating appropriate machine code. I don’t know which is the preferably strategy, though. Both have trade-offs. The HotSpot JIT compiler, for the JVM, can use a simpler code-generation mechanism (it doesn’t need to determine operand types, because they’re already encoded in the instruction), but that means it needs a more complex bytecode format, with more instruction types.
I’ve been using Java (and admiring the JVM) for about ten years now.
But, in my opinion, the CLR is now the superior implementation, in almost every way.