Il y a beaucoup de similitudes entre les deux implémentations (et à mon avis: oui, ce sont toutes les deux des "machines virtuelles").
D'une part, ce sont toutes deux des VM basées sur la pile, sans notion de «registres» comme nous avons l'habitude de voir dans un processeur moderne comme le x86 ou PowerPC. L'évaluation de toutes les expressions ((1 + 1) / 2) est effectuée en poussant des opérandes sur la "pile", puis en faisant sortir ces opérandes de la pile chaque fois qu'une instruction (ajouter, diviser, etc.) a besoin de consommer ces opérandes. Chaque instruction repousse ses résultats sur la pile.
C'est un moyen pratique d'implémenter une machine virtuelle, car à peu près tous les processeurs du monde ont une pile, mais le nombre de registres est souvent différent (et certains registres sont à usage spécial, et chaque instruction attend ses opérandes dans différents registres, etc. ).
Donc, si vous voulez modéliser une machine abstraite, un modèle purement basé sur la pile est une très bonne solution.
Bien sûr, les vraies machines ne fonctionnent pas de cette façon. Ainsi, le compilateur JIT est chargé d'effectuer "l'enregistrement" des opérations de bytecode, en planifiant essentiellement les registres CPU réels pour contenir les opérandes et les résultats chaque fois que possible.
Donc, je pense que c'est l'un des plus grands points communs entre le CLR et la JVM.
Quant aux différences ...
Une différence intéressante entre les deux implémentations est que le CLR comprend des instructions pour créer des types génériques, puis pour appliquer des spécialisations paramétriques à ces types. Ainsi, au moment de l'exécution, le CLR considère un List <int> comme un type complètement différent d'un List <String>.
Sous les couvertures, il utilise le même MSIL pour toutes les spécialisations de type de référence (donc un List <String> utilise la même implémentation qu'un List <Object>, avec des types différents aux limites de l'API), mais chaque type de valeur utilise sa propre implémentation unique (List <int> génère un code complètement différent de List <double>).
En Java, les types génériques sont purement une astuce du compilateur. La machine virtuelle Java n'a aucune idée des classes qui ont des arguments de type et elle ne peut pas effectuer de spécialisations paramétriques au moment de l'exécution.
D'un point de vue pratique, cela signifie que vous ne pouvez pas surcharger les méthodes Java sur des types génériques. Vous ne pouvez pas avoir deux méthodes différentes, avec le même nom, différant uniquement selon qu'elles acceptent une List <String> ou une List <Date>. Bien sûr, puisque le CLR connaît les types paramétriques, il n'a aucun problème à gérer les méthodes surchargées sur les spécialisations de types génériques.
Au quotidien, c'est la différence que je remarque le plus entre le CLR et la JVM.
D'autres différences importantes incluent:
Le CLR a des fermetures (implémentées en tant que délégués C #). La JVM ne prend en charge les fermetures que depuis Java 8.
Le CLR a des coroutines (implémentées avec le mot clé C # 'yield'). La JVM ne le fait pas.
Le CLR permet au code utilisateur de définir de nouveaux types de valeur (structs), tandis que la JVM fournit une collection fixe de types de valeur (byte, short, int, long, float, double, char, boolean) et permet uniquement aux utilisateurs de définir une nouvelle référence- types (classes).
Le CLR prend en charge la déclaration et la manipulation des pointeurs. Ceci est particulièrement intéressant car la JVM et le CLR utilisent des implémentations strictes de ramasse-miettes de compactage générationnel comme stratégie de gestion de la mémoire. Dans des circonstances ordinaires, un GC compactage strict a vraiment du mal avec les pointeurs, car lorsque vous déplacez une valeur d'un emplacement mémoire à un autre, tous les pointeurs (et pointeurs vers des pointeurs) deviennent invalides. Mais le CLR fournit un mécanisme "d'épinglage" afin que les développeurs puissent déclarer un bloc de code dans lequel le CLR n'est pas autorisé à déplacer certains pointeurs. C'est très pratique.
La plus grande unité de code dans la JVM est soit un 'package' comme en témoigne le mot-clé 'protected' ou sans doute un JAR (c'est-à-dire Java ARchive) comme en témoigne la possibilité de spécifier un fichier jar dans le chemin de classe et de le traiter comme un dossier du code. Dans le CLR, les classes sont agrégées dans des «assemblys» et le CLR fournit une logique de raisonnement et de manipulation des assemblys (qui sont chargés dans «AppDomains», fournissant des sandbox au niveau des sous-applications pour l'allocation de mémoire et l'exécution de code).
Le format de bytecode CLR (composé d'instructions MSIL et de métadonnées) a moins de types d'instructions que la JVM. Dans la JVM, chaque opération unique (ajouter deux valeurs int, ajouter deux valeurs flottantes, etc.) a sa propre instruction unique. Dans le CLR, toutes les instructions MSIL sont polymorphes (ajoutez deux valeurs) et le compilateur JIT est responsable de la détermination des types d'opérandes et de la création du code machine approprié. Je ne sais pas quelle est la stratégie de préférence. Les deux ont des compromis. Le compilateur HotSpot JIT, pour la JVM, peut utiliser un mécanisme de génération de code plus simple (il n'a pas besoin de déterminer les types d'opérandes, car ils sont déjà codés dans l'instruction), mais cela signifie qu'il a besoin d'un format de bytecode plus complexe, avec plus de types d'instructions.
J'utilise Java (et j'admire la JVM) depuis une dizaine d'années maintenant.
Mais, à mon avis, le CLR est maintenant l'implémentation supérieure, à presque tous les égards.