Il existe quelques outils ( Excelsior JET , etc.) qui prétendent transformer Java en exécutables natifs (*.exe
). Cependant, je comprends que ces outils ne font que créer des wrappers natifs qui invoquent/exécutent Java
à partir d'un shell ou d'une ligne de commande.
Si cette compréhension est incorrecte, je ne vois pas comment cela pourrait être. Si une JVM en cours d'exécution (Java
processus) est essentiellement un interpréteur hautes performances, chargeant le bytecode à partir de Java classfiles à la volée, alors je ne vois pas comment un Java (une collection de fichiers de bytecode qui servent d'entrée à une JVM) pourrait jamais être vraiment convertie en un exécutable.
En effet, le processus JVM est déjà un exécutable natif qui prend en entrée des ensembles de fichiers de bytecode. Pour fusionner ces fichiers de bytecode et le processus JVM en un seul exécutable natif unifié ne semble pas possible sans réécrire complètement la JVM et le dé-railing de la spécification JVM.
Je demande donc: comment ces outils réellement "transforment" Java fichiers de classe en un exécutable natif, ou font-ils ?
Tous les programmes ont un environnement d'exécution. Nous avons tendance à oublier cela, mais c'est là. Lib standard pour C qui encapsule les appels système vers le système d'exploitation. Objective-C a son runtime qui encapsule tout son message qui passe.
Avec Java, le runtime est la JVM. La plupart des implémentations Java que les gens connaissent bien sont similaires à la machine virtuelle Java HotSpot qui est un interpréteur de code octet et un compilateur JIT.
Cela ne doit pas être la seule implémentation. Rien ne dit que vous ne pouvez pas construire un runtime lib-esque standard pour Java et compiler le code en code machine natif et l'exécuter dans le runtime qui gère les appels de nouveaux objets dans mallocs et accès aux fichiers dans les appels système sur la machine. Et c'est ce que fait le compilateur Ahead Of Time (AOT plutôt que JIT). Appelez ce runtime comme vous voulez ... vous pouvez l'appeler une implémentation JVM (et il le fait suivre la spécification JVM) ou un environnement d'exécution ou une bibliothèque standard pour Java. C'est là et ça fait essentiellement la même chose.
Cela pourrait être fait soit en réimplémentant javac
pour cibler la machine native (c'est un peu ce que GCJ a fait). Ou cela pourrait être fait en traduisant le code d'octet généré par javac
en code machine (ou octet) pour une autre machine - c'est ce que fait Android. Basé sur Wikipedia c'est aussi ce que fait Excelsior JET ("Le compilateur transforme le portable Java octet code en exécutables optimisés pour le matériel et le système d'exploitation (OS)" souhaités), et il en va de même pour RoboVM .
Il y a des complications supplémentaires avec Java cela signifie que c'est très difficile à faire comme approche exclusive. Le chargement dynamique des classes (class.forName()
) ou objets mandatés nécessite une dynamique que les compilateurs AOT ne fournissent pas facilement et leurs JVM respectives doivent donc également inclure soit un compilateur JIT (Excelsior JET) soit un interprète (GCJ) pour gérer les classes qui ne pouvaient pas être précompilées en natif.
Rappelez-vous, le JVM est une spécification , avec de nombreuses implémentations . La bibliothèque standard C est également une spécification avec de nombreuses implémentations différentes.
Avec Java8, un bon travail a été fait sur la compilation AOT. Au mieux, on ne peut résumer AOT en général que dans les limites de textbox. Cependant, lors du JVM Language Summit pour 2015 (août 2015), il y avait une présentation: Java Goes AOT (vidéo youtube). Cette vidéo dure 40 minutes et aborde de nombreux aspects techniques plus approfondis et des performances de référence.
gcj
exemple exécutable minimal
Vous pouvez également observer une implémentation open source comme gcj
(désormais obsolète). Par exemple. Fichier Java:
public class Main {
public static void main(String args[]) {
System.out.println("hello world");
}
}
Ensuite, compilez et exécutez avec:
gcj -c Main.Java
gcj --main=Main -o Main Main.o
./Main
Vous êtes maintenant libre de le décompiler et de voir comment cela fonctionne.
file Main.o
Indique qu'il s'agit d'un fichier elfe.
readelf -d Main | grep NEEDED
Dit que cela dépend des bibliothèques dynamiques:
0x0000000000000001 (NEEDED) Shared library: [libgcj.so.14]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
Donc libgcj.so doit être l'endroit où la fonctionnalité Java est implémentée.
Vous pouvez ensuite le décompiler avec:
objdump -Cdr Main.o
et voyez exactement comment il est mis en œuvre.
Ressemble beaucoup au C++, à beaucoup d'appels de noms et de fonctions polymorphes indirectes.
Je me demande comment la collecte des ordures intervient. Cela vaut la peine d'être étudié: https://stackoverflow.com/questions/7100776/garbage-collection-implementation-in-compiled-languages et d'autres langues compilées avec GC comme Go.
Testé sur Ubuntu 14.04, GCC 4.8.4.
Jetez également un coup d'œil à https://en.wikipedia.org/wiki/Android_Runtime , l'épine dorsale de Android 5 à partir de maintenant, qui fait le plein AOT pour optimiser les applications Android.