L'élément clé est la séparation de la compilation de la phase d'exécution. Par cela, il est possible d'écrire d'autres compilateurs compilant d'autres langues en bytecode.
Le bytecode agit comme le code machine d'un CPU - vous avez toutes les petites opérations nécessaires pour exécuter un programme - vous pouvez obtenir une variable, faire des calculs dessus, avoir des opérations conditionnelles, etc.
Java n'est pas non plus spécial. En Java, l'existence de plusieurs langages n'était même pas un objectif de conception, contrairement aux autres machines virtuelles. Pour .Net CIL de Microsoft, la capacité d'exécuter plusieurs langues (C #, VB.Net, ...) était un élément clé de la conception, ainsi que le ParrotVM du projet Perl6 visant à être une machine virtuelle générique.
Pour le plaisir, j'ai créé une fois une preuve que même le moteur Zend de PHP le permettrait.
Et franchement, ce n'est pas quelque chose de nouveau - même sur du vrai matériel, vous pouvez exécuter plusieurs langues - c'est-à-dire C ou Fortran.
La différence par rapport à cette séparation de la compilation et de l'exécution sont les interprètes clssic, comme certaines formes de Basic, les scripts shell, etc. ils fonctionnent souvent de manière à exécuter le code plus ou moins ligne par ligne sans le mettre sous une forme immédiate entre.