Hay muchas similitudes entre ambas implementaciones (y en mi opinión: sí, ambas son "máquinas virtuales").
Por un lado, ambas son máquinas virtuales basadas en pila, sin la noción de "registros" como estamos acostumbrados a ver en una CPU moderna como la x86 o PowerPC. La evaluación de todas las expresiones ((1 + 1) / 2) se realiza insertando operandos en la "pila" y luego sacando esos operandos de la pila cada vez que una instrucción (agregar, dividir, etc.) necesita consumir esos operandos. Cada instrucción devuelve sus resultados a la pila.
Es una forma conveniente de implementar una máquina virtual, porque casi todas las CPU del mundo tienen una pila, pero la cantidad de registros a menudo es diferente (y algunos registros tienen un propósito especial, y cada instrucción espera sus operandos en diferentes registros, etc. )
Entonces, si va a modelar una máquina abstracta, un modelo puramente basado en pila es una muy buena manera de hacerlo.
Por supuesto, las máquinas reales no funcionan de esa manera. Por lo tanto, el compilador JIT es responsable de realizar el "registro" de las operaciones de código de bytes, esencialmente programando los registros reales de la CPU para contener operandos y resultados siempre que sea posible.
Entonces, creo que ese es uno de los mayores puntos en común entre CLR y JVM.
En cuanto a las diferencias ...
Una diferencia interesante entre las dos implementaciones es que el CLR incluye instrucciones para crear tipos genéricos y luego para aplicar especializaciones paramétricas a esos tipos. Entonces, en tiempo de ejecución, el CLR considera que una Lista <int> es un tipo completamente diferente de una Lista <String>.
Debajo de las cubiertas, usa el mismo MSIL para todas las especializaciones de tipo de referencia (por lo que una Lista <String> usa la misma implementación que una Lista <Objeto>, con diferentes tipos de conversión en los límites de la API), pero cada tipo de valor usa su propia implementación única (List <int> genera un código completamente diferente de List <double>).
En Java, los tipos genéricos son un truco puramente de compilación. La JVM no tiene idea de qué clases tienen argumentos de tipo, y no puede realizar especializaciones paramétricas en tiempo de ejecución.
Desde una perspectiva práctica, eso significa que no puede sobrecargar los métodos Java en tipos genéricos. No puede tener dos métodos diferentes, con el mismo nombre, que difieren solo en si aceptan una Lista <String> o una Lista <Fecha>. Por supuesto, dado que el CLR conoce los tipos paramétricos, no tiene problemas para manejar métodos sobrecargados en especializaciones de tipos genéricos.
En el día a día, esa es la diferencia que más noto entre el CLR y la JVM.
Otras diferencias importantes incluyen:
El CLR tiene cierres (implementados como delegados de C #). La JVM solo admite cierres desde Java 8.
El CLR tiene corutinas (implementadas con la palabra clave 'rendimiento' C #). La JVM no lo hace.
El CLR permite que el código de usuario defina nuevos tipos de valores (estructuras), mientras que la JVM proporciona una colección fija de tipos de valores (byte, short, int, long, float, double, char, boolean) y solo permite a los usuarios definir nuevas referencias. tipos (clases).
El CLR proporciona soporte para declarar y manipular punteros. Esto es especialmente interesante porque tanto la JVM como la CLR emplean implementaciones estrictas de recolección de basura de compactación generacional como su estrategia de administración de memoria. En circunstancias normales, un GC de compactación estricto tiene dificultades con los punteros, porque cuando mueve un valor de una ubicación de memoria a otra, todos los punteros (y punteros a punteros) se vuelven inválidos. Pero el CLR proporciona un mecanismo de "fijación" para que los desarrolladores puedan declarar un bloque de código dentro del cual el CLR no puede mover ciertos punteros. Es muy conveniente.
La unidad de código más grande en la JVM es un 'paquete' como lo demuestra la palabra clave 'protegida' o posiblemente un JAR (es decir, Java ARchive) como puede ser capaz de especificar un jar en el classpath y tratarlo como una carpeta de código En el CLR, las clases se agregan en 'ensamblajes', y el CLR proporciona lógica para razonar y manipular ensamblajes (que se cargan en "AppDomains", proporcionando entornos limitados a nivel de sub-aplicación para la asignación de memoria y la ejecución de código).
El formato de código de bytes CLR (compuesto por instrucciones y metadatos MSIL) tiene menos tipos de instrucciones que la JVM. En la JVM, cada operación única (agregar dos valores int, agregar dos valores flotantes, etc.) tiene su propia instrucción única. En el CLR, todas las instrucciones de MSIL son polimórficas (agregue dos valores) y el compilador JIT es responsable de determinar los tipos de operandos y crear el código de máquina apropiado. Sin embargo, no sé cuál es la estrategia preferible. Ambos tienen compensaciones. El compilador JIT de HotSpot, para la JVM, puede usar un mecanismo de generación de código más simple (no necesita determinar los tipos de operandos, porque ya están codificados en la instrucción), pero eso significa que necesita un formato de código de bytes más complejo, con más tipos de instrucción
He estado usando Java (y admirando la JVM) durante unos diez años.
Pero, en mi opinión, el CLR es ahora la implementación superior, en casi todos los sentidos.