I. Introduction▲
OpenJDK est un projet volumineux. Il comprend de nombreux projets différents, écrits dans plusieurs langages, principalement Java, C et C++. Cet article a pour vocation de présenter le développement dans OpenJDK en commençant par le projet HotSpot, puis le projet JDK. Nous verrons comment ajouter et modifier des classes dans le JDK et des nouveaux paramètres dans la machine virtuelle (VM). Je vous invite à lire mon précédent article sur l'installation et la compilation d'OpenJDK 8, qui vous permettra de commencer avec un environnement stable. Cet article fait partie d'une série dédiée à OpenJDK 8.
II. Développement dans HotSpot▲
II-A. Introduction▲
HotSpot est la machine virtuelle Java sur laquelle repose tout programme développé dans ce langage. C'est elle qui gère les threads, les appels systèmes, les ramasse-miettes… et son rôle est plus que primordial. Elle est écrite en C++ et contient approximativement 250 000 lignes de code. Notre objectif va être de modifier le code source de celle-ci afin d'ajouter nos propres paramètres VM. Ces paramètres apportent une flexibilité dans le développement, offrant aux utilisateurs comme aux développeurs la possibilité de configurer finement la machine virtuelle lors de l'exécution d'un programme.
II-B. Ajouter un paramètre dans la machine virtuelle▲
Les paramètres VM sont un ensemble d'arguments permettant de paramétrer à souhait HotSpot. Par exemple le paramètre -XX:+PrintGCDetails permet de donner des informations sur la mémoire utilisée par les GC. Notre objectif dans cette partie va être d'ajouter un nouveau paramètre dans la machine virtuelle afin de délimiter notre code. Dès lors que nous voudrons ajouter ou modifier du code existant, nous nous assurerons que notre paramètre est bien activé, afin d'éviter d'éventuels effets de bord ou oublis.
Vous allez voir qu'ajouter un paramètre est très simple. Ouvrez le fichier globals.hpp, situé dans le répertoire src/share/vm/runtime/.
Selon le type de compilation (debug, product…) les paramètres ont des visibilités différentes. Cela est réalisé durant la compilation grâce à un ensemble de macros détaillées ci-dessous. La déclaration d'un nouveau paramètre se fait toujours par le biais de l'une de ces macros.
Les paramètres peuvent avoir quatre types distincts :
- intx : un entier ;
- uintx : un long ;
- ccstr : alias pour une chaîne de caractères constante (const char*) ;
- bool : un booléen.
La déclaration d'un nouveau paramètre se fait grâce à l'une des macros suivantes :
- product : le paramètre est toujours accessible et modifiable ;
- develop : le paramètre est accessible et modifiable en mode développement et est une constante en mode production ;
- experimental : le paramètre est toujours accessible. Cette macro sert à tester de nouvelles fonctionnalités pendant le développement. Pour activer les paramètres de type experimental, il faut lancer la VM avec le paramètre -XX:+UnlockExperimentalVMOptions. Sans cela, il ne sera pas accessible ;
- lp64_product : le paramètre sera toujours une constante dans une version 32 bits de la VM ;
- notproduct : le paramètre est accessible et modifiable en mode développement et n'existe pas en mode production ;
- diagnostic : le paramètre est toujours accessible. Cette macro sert pour diagnostiquer des bogues et s'assurer de la qualité de la machine virtuelle. Pour activer les paramètres de type diagnostic, il faut lancer la VM avec le paramètre -XX:+UnlockDiagnosticVMOptions. Sans cela, il ne sera pas accessible ;
- manageable : le paramètre est toujours accessible et modifiable. De plus, il peut être dynamiquement modifié durant l'exécution de la VM, par le biais de la JConsole ou JMX (via com.sun.management.HotSpotDiagnosticMXBean) ;
- product_rw : le paramètre est toujours accessible et modifiable. De plus, il peut être dynamiquement modifié durant l'exécution de la VM. Cela ressemble à manageable sauf que l'intérêt de ce type de paramètre est pour un usage privé.
Il existe deux autres macros spécifiques develop_pd et product_pd dont nous ne détaillerons pas l'intérêt ici. Dans la majorité des cas, ce sont les macros product, develop, diagnostic et experimental qui sont utilisées.
Pour nos modifications, nous nous servirons d'un paramètre de type product et booléen.
product(bool
, TutorialGC, false
, "Used for this tutorial"
) \
Les paramètres à donner permettent de spécifier respectivement, le type de données, le nom du paramètre, la valeur par défaut et la description. La valeur par défaut sera faux, nous forçant à activer le paramètre en utilisant -XX:+TutorialGC. Ainsi, nos modifications ne seront pas prises en compte lors d'un lancement classique.
N'oubliez pas d'ajouter des « \ » pour chaque ligne vide et à la fin de la déclaration du paramètre, comme cela est fait dans le code existant.
Afin de valider ce que nous avons modifié, recompiler le projet et exécuter un programme Java quelconque (un Hello World suffit) et utilisant le paramètre -XX:+TutorialGC.
Attention, nous allons modifier la machine virtuelle pour effectuer notre premier Hello World. Vous pouvez recompiler après les modifications à venir.
La compilation du projet se fait tout simplement de la manière suivante :
make
Grâce à l'utilitaire ccache installé préalablement, la recompilation des sources ne vous forcera pas à compiler la totalité du projet OpenJDK, seulement HotSpot, et une partie du JDK.
Les lignes de commandes de compilation et de lancement pour une classe Test.java sont :
2.
./javac Test.java
./java -XX:+TutorialGC Test
Faites bien attention à vous placer dans le répertoire des images, situé dans le répertoire build afin d'utiliser la version compilée, et non la version installée qui ne contient pas nos modifications.
Si le paramètre a mal été ajouté, ou que vous n'avez pas recompilé correctement, l'erreur suivante s'affichera :
2.
3.
Unrecognized VM option 'TutorialGC'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
II-C. Hello World dans la machine virtuelle▲
Nous allons effectuer notre premier ajout de code, autre qu'un paramètre dans HotSpot. Le but est d'afficher un message lors du démarrage de la machine virtuelle. Pour cela, ouvrez le fichier init.cpp situé dans le répertoire src/share/vm/runtime. Ajoutez le code suivant à la fin de la méthode init_globals().
2.
3.
if
(TutorialGC) {
gclog_or_tty->
print("La machine virtuelle démarre..."
);
}
La variable globale gclog_or_tty correspond à un logger général qui va écrire dans la console ou dans un fichier ce que vous donnez en paramètre. Si vous spécifiez le paramètre loggc au démarrage de la machine virtuelle, le contenu sera écrit dans un fichier, sinon dans la console, et est donné ainsi -Xloggc:logs/gc_log.log où « logs » correspond au répertoire et gc_log.log est le contenu du fichier de log. Ainsi, HotSpot écrira le contenu dans le fichier situé dans logs/gc_log.log.
Vous pouvez maintenant recompiler HotSpot et lancer un programme Java arbitraire, comme dans le chapitre précédent. Vous verrez alors apparaître au démarrage « La machine virtuelle démarre… ». J'ai effectué ce test avec un simple Hello World et le résultat affiché est :
La machine virtuelle démarre...Hello World !
Notre code peut donc être activé ou désactivé à volonté, mettant ainsi en exergue l'utilité des paramètres. Et pour la petite anecdote, sachez qu'il existe plus de 1200 paramètres dans HotSpot pour configurer cette dernière à volonté.
III. Ajout de classes dans le JDK▲
Dans cette partie de l'article, nous allons voir comment ajouter notre propre classe dans le JDK et la lier à la compilation. Lors de la création des images, notre classe sera alors directement intégrée aux JAR et accessible comme toutes les autres classes, telles que System ou String.
Dans de nombreux projets, un ensemble de classes utilitaires est toujours développé afin de faciliter le développement. La bibliothèque Apache Commons Lang se retrouve très régulièrement dans les projets et embarque un nombre impressionnant de classes utilitaires. Notre objectif va être d'intégrer la classe SystemUtils d'Apache dans le JDK. J'ai choisi cette classe, car elle ne possède que peu de dépendances externes, donc ne nécessite que très peu de modifications en vue de son intégration.
Il faut bien comprendre que si nous voulons ajouter une « vraie » classe, il faut faire attention à ce à quoi elle est liée et surtout la liste des classes de la bibliothèque qu'elle utilise. Si une classe utilise cinq autres classes du projet Apache Commons Lang, alors nous devons intégrer les cinq autres, qui peuvent, elles aussi, dépendre d'autres classes et ainsi de suite. On se retrouve alors à ajouter une partie entière de la bibliothèque pour une seule classe à la base. Enfin, si vous désirez ajouter et coder votre propre classe, la façon de faire est absolument identique, à la différence près que vous n'aurez aucune dépendance, sauf sur votre propre code et les classes du JDK, ce qui ne pose aucun problème.
Tout d'abord, voyons les principaux répertoires qui vont nous intéresser :
- jdk8/jdk/src/share/classes : contient toutes les classes Java du JDK ;
- jdk8/jdk/src/share/native : contient le code natif, écrit en C, des classes Java. Nous ne nous servirons pas de ce répertoire, mais il peut être utile de savoir où il se trouve. Pour chaque classe ayant au moins une méthode native, vous devez créer un fichier C ayant le même nom que la classe et y inclure votre méthode selon un nommage précis. Le code est alors exécuté dans cette méthode, et peut être lié à la machine virtuelle. C'est comme cela que certaines méthodes du JDK donnent des informations sur HotSpot, comme System.gc() ;
- jdk8/jdk/make/java/java : contient les fichiers de propriétés pour la compilation des classes Java. La déclaration des classes à compiler, des fichiers C, des méthodes natives… Tout cela se fait ici.
Nous allons donc ajouter nos classes dans jdk8/jdk/src/share/classes, puis déclarer que nous devons les compiler et les ajouter comme les autres dans le JDK, grâce aux fichiers situés dans jdk8/jdk/make/java/java.
Passons à la pratique. Télécharger les sources du projet à l'adresse suivante : http://commons.apache.org/proper/commons-lang/download_lang.cgi. Pour cet article, j'ai utilisé la version 3.1.
Puisque nous désirons ajouter une classe utilitaire, nous placerons celle-ci dans le package java.util. Cela n'est en rien une nécessité, mais un choix personnel. Copier les classes SystemUtils et JavaVersion, situées dans src/main/java/org/apache/commons/lang3 dans le répertoire jdk8/jdk/src/share/classes/java/util. Nous devons aussi ajouter l'énumération JavaVersion, car elle est utilisée dans SystemUtils.
Ces deux classes étaient situées dans le package org.apache.commons.lang3, or nous les avons déclarées dans java.util. Nous devons donc éditer les deux classes et remplacer le code suivant :
package
org.apache.commons.lang3;
par celui-ci :
package
java.util;
Une fois cela fait, allez dans le répertoire jdk8/jdk/make/java/java. Voici une description des principaux fichiers de compilation :
- FILES_java.gmk : contient la liste des classes Java à compiler. Cette liste n'est pas complète, vous pourrez trouver d'autres fichiers comme celui-ci pour certaines implémentations Sun par exemple ;
- FILES_c.gmk : contient la liste des fichiers C à compiler. Les fichiers contiennent le code des méthodes déclarées natives dans une classe. Par exemple la méthode Runtime.gc() est native, vous trouverez donc le code C associé dans le fichier Runtime.c avec comme nom de méthode « Java_java_lang_Runtime_gc » ;
- Exportedfiles.gmk : contient la liste des classes Java pour lesquelles le compilateur doit générer un header C (.h). Ce header généré permet d'appeler des méthodes définies dans un autre fichier. Par exemple, vous pouvez importer le header associé à Runtime, pour faire exécuter des méthodes natives de cette classe ;
- mapfile-vers : contient la liste des méthodes natives de toutes les classes Java. Lorsque vous déclarez une méthode native, celle-ci doit se retrouver dans ce fichier.
Puisque nos deux classes ne contiennent pas de méthodes natives, nous avons uniquement besoin de les déclarer dans le fichier FILES_java.gmk. Ajoutez deux lignes, sur le même modèle que les autres classes, pour nos deux nouvelles classes, soit java/util/SystemUtils.java et java/util/JavaVersion.java. N'oubliez pas d'ajouter des « \ » à la fin de chaque ligne.
Une fois cela fait, il ne vous reste plus qu'à compiler le JDK en exécutant la commande make, comme fait précédemment.
Pour tester que l'ajout fonctionne bien, il vous suffit de créer une classe dans laquelle vous appelez des méthodes ou des constantes de la classe SystemUtils.
2.
3.
4.
5.
6.
7.
8.
import
java.util.SystemUtils;
public
class
Test {
public
static
void
main
(
String[] args) {
System.out.println
(
SystemUtils.JAVA_VM_VERSION);
}
}
Sur ma machine, le résultat affiché est « 25.0-b39 », correspondant bien à la version compilée, affichée grâce à l'option « java -version » et me donnant comme affichage :
openjdk version "1.8.0-internal"
OpenJDK Runtime Environment (
build 1
.8
.0
-internal-olivier_2013_06_30_00_34-b00)
OpenJDK 64
-Bit Server VM (
build 25
.0
-b39, mixed mode)
IV. Conclusion▲
Nous avons vu comment ajouter des paramètres dans la machine virtuelle HotSpot et des classes dans le JDK. Des explications ont été fournies sur le fonctionnement de ces parties du projet OpenJDK ainsi qu'une description des différents fichiers et répertoires utiles au développement. Il ne vous reste plus qu'à modifier le code et à vous amuser à découvrir l'intérieur d'une plateforme utilisée par des millions d'utilisateurs quotidiennement.
V. Remerciements▲
Je tiens à remercier mes professeurs Julien Sopena et Gael Thomas de m'avoir permis de faire un stage sur la machine virtuelle HotSpot, qui donna lieu à cet article ainsi que Mickael Baron pour sa relecture technique et Claude LELOUP pour la correction orthographique.