OpenJDK envisage des traces de pile asynchrones pour Java,
elles permettent d'inclure des informations sur les trames de pile Java et natives

Une proposition visant à promouvoir les traces de pile asynchrones dans Java prospére dans le processus d'amélioration de Java de l'OpenJDK. Appelé Asynchronous Stack Trace VM API, le projet consisterait à définir une API AsyncGetStackTrace pour collecter les traces de pile de manière asynchrone et inclure des données sur les trames de pile Java et natives.

Selon la proposition, les performances ne seraient pas affectées lorsque l'API n'est pas utilisée et les besoins en mémoire ne seraient pas sensiblement augmentés par rapport à l'API AsyncGetCallTrace existante. La nouvelle API ne serait pas recommandée pour une utilisation en production, car elle pourrait faire planter la JVM. Les plans prévoient de minimiser les risques d'un tel incident par le biais de tests et de vérifications approfondies.


Actuellement, AsyncGetCallTrace est utilisé par la plupart des profileurs disponibles, tant open source que commerciaux, y compris async-profiler. Mais il présente deux inconvénients majeurs.

  • il s'agit d'une API interne, non exportée dans un quelconque en-tête ;
  • elle ne renvoie que des informations sur les cadres Java, à savoir leurs indices de méthode et de bytecode.

Nom : openjdk.jpg
Affichages : 17830
Taille : 10,4 Ko

Ces problèmes rendent plus difficile l'implémentation de profileurs et d'outils connexes. Bien que des informations supplémentaires puissent être extraites de la VM HotSpot par le biais d'un code complexe, via un code complexe, mais d'autres informations utiles sont dissimulées et impossibles à obtenir :

  • Si une trame Java compilée est inlined (actuellement, on ne peut l'obtenir que pour les trames compilées les plus hautes) ;
  • Le niveau de compilation d'une trame Java (c'est-à-dire, compilée par C1 ou C2) ;
  • Des informations sur les trames C/C++ qui ne sont pas au sommet de la pile.

Ces données peuvent être utiles lors du profilage et du réglage d'une VM pour une application donnée, ainsi que pour le profilage de code utilisant fortement JNI.

L'API AsyncGetStackTrace serait calquée sur l'API AsyncGetCallTrace. La nouvelle API n'a pas encore été proposée pour une version spécifique de Java standard. La prochaine version de Java est le kit de développement Java (JDK) 20, qui est attendu en mars 2023. Java dispose d'un processus officiel permettant d'intégrer des changements dans la plateforme qui a réussi à rester réactive face à l'évolution des circonstances tout en atteignant un haut degré de stabilité.

Description

L'équipe encharge de Java propose une nouvelle API AsyncGetStackTrace, modelée sur l'API AsyncGetCallTrace :

Code Java : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
void AsyncGetStackTrace(CallTrace *trace, jint depth, void* ucontext,
                        uint32_t options);


Cette API peut être appelée par les profileurs pour obtenir la trace de la pile pour le thread en cours d'execution. L'appel de cette API à partir d'un gestionnaire de signaux est sûr, et la nouvelle implémentation sera au moins aussi stable que AsyncGetCallTrace ou le code de suivi de pile de la JFR. La VM remplit les informations sur les trames et le nombre de trames. L'appelant de l'API doit allouer le tableau CallTrace avec suffisamment de mémoire pour la profondeur de pile demandée.

Paramètres

  • trace - tampon pour les données structurées à remplir par la VM ;
  • depth - profondeur maximale de la trace de la pile d'appels ;
  • ucontext - optionnel ucontext_t du thread actuel lorsqu'il a été interrompu ;
  • options - bit défini pour les options

Actuellement, seul le bit le plus bas des options est pris en compte : Il active (1) ou désactive (0) l'inclusion des trames C/C++. Tous les autres bits sont considérés comme étant à 0.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
typedef struct {
  jint num_frames;                // number of frames in this trace
  CallFrame *frames;              // frames
  void* frame_info;               // more information on frames
} CallTrace;

La structure de la trace est rempli par la VM. Son champ num_frames contient le nombre réel d'images dans le tableau des images ou un code d'erreur. Le champ frame_info de cette structure peut être utilisé ultérieurement pour stocker plus d'informations, mais il est actuellement NULL. Les codes d'erreur sont un sous-ensemble des codes d'erreur pour AsyncGetCallTrace, avec l'ajout de THREAD_NOT_JAVA lié à l'appel de cette procédure pour des threads non-Java :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
enum Error {
  NO_JAVA_FRAME         =   0,
  NO_CLASS_LOAD         =  -1, 
  GC_ACTIVE             =  -2,    
  UNKNOWN_NOT_JAVA      =  -3,
  NOT_WALKABLE_NOT_JAVA =  -4,
  UNKNOWN_JAVA          =  -5,
  UNKNOWN_STATE         =  -7,
  THREAD_EXIT           =  -8,
  DEOPT                 =  -9,
  THREAD_NOT_JAVA       = -10
};

Chaque CallFrame est l'élément d'une union, puisque les informations stockées pour les frames Java et non-Java diffèrent :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
typedef union {
  FrameTypeId type;     // to distinguish between JavaFrame and NonJavaFrame 
  JavaFrame java_frame;
  NonJavaFrame non_java_frame;
} CallFrame;
On peut distinguer plusieurs types de frames :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
enum FrameTypeId : uint8_t {
  FRAME_JAVA         = 1, // JIT compiled and interpreted
  FRAME_JAVA_INLINED = 2, // inlined JIT compiled
  FRAME_NATIVE       = 3, // native wrapper to call C methods from Java
  FRAME_STUB         = 4, // VM generated stubs
  FRAME_CPP          = 5  // C/C++/... frames
};

Les deux premiers types sont destinés aux frames Java, pour lesquels nous stockons les informations suivantes dans une structure de type JavaFrame :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
typedef struct {     
  FrameTypeId type;       // frame type
  int8_t comp_level;      // compilation level, 0 is interpreted
  uint16_t bci;           // 0 < bci < 65536
  jmethodID method_id;
} JavaFrame;              // used for FRAME_JAVA, FRAME_JAVA_INLINED and FRAME_NATIVE

Le comp_level indique le niveau de compilation de la méthode liée à la trame, les chiffres les plus élevés représentant des niveaux de compilation supérieurs. Il est calqué sur CompLevel enum de HotSpot mais dépend de l'infrastructure du compilateur utilisé. Une valeur de zéro indique l'absence de compilation, c'est-à-dire l'interprétation du bytecode.

Les informations sur tous les autres frames sont stockées dans les structs NonJavaFrame :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
typedef struct {
  FrameTypeId type;  // frame type
  void *pc;          // current program counter inside this frame
} NonJavaFrame;

Bien que l'API fournisse plus d'informations, l'espace requis par trame (par exemple, 16 octets sur x86) est le même que pour l'API AsyncGetCallTrace existante. Le fait de renvoyer des informations sur les trames C/C++ entraîne une divulgation des détails de l'implémentation, mais cela est également vrai pour les trames Java d'AsyncGetCallTrace puisqu'elles divulguent les détails de l'implémentation des fichiers de la bibliothèque standard et incluent des trames de wrapper natives.

Les informations sur tous les autres framessont stockées dans les structs NonJavaFrame :

Amélioration de la plateforme Java

JCP : Java Community Process

Même un développeur Java de longue date peut ne pas avoir une bonne compréhension de la manière dont la plateforme est développée et maintenue. La principale leçon à retenir est qu'il s'agit vraiment d'un processus ouvert.

À la base du développement de Java, se trouve le Java Community Process (JCP). Il s'agit d'une sorte de document de base auto-conscient qui définit la manière d'introduire des modifications dans la plateforme et qui permet également de modifier le processus lui-même. La dernière version du JCP est la 2.11, qui a été adoptée en 2019.

Le JCP formalise la manière dont les nouvelles fonctionnalités et les modifications apportées à Java (c'est-à-dire les spécifications techniques) sont proposées, examinées et approuvées, y compris la définition de divers rôles que les gens peuvent occuper. Ces rôles permettent d'offrir un lieu où la communauté des utilisateurs de Java peut participer à la gouvernance de la plateforme.

JSR : Java Specification Request

Pour proposer de nouvelles fonctionnalités et des changements, le JCP permet la création ("initiation") de Java Specification Requests (JSR). Cela se fait via un formulaire standardisé. Pour accéder au formulaire, vous devez vous inscrire pour obtenir un compte JCP gratuit.

À partir de là, de nombreux changements, modestes ou non, trouvent leur chemin dans les technologies Java que nous utilisons tous les jours. Lorsqu'une JSR arrive, elle entre dans le processus de révision des JSR. Il s'agit d'un processus en plusieurs étapes dans lequel les changements proposés dans le JSR sont progressivement examinés plus sérieusement, modifiés, et finalement adoptés ou mis en attente.

JEP : proposition d'amélioration du JDK

Le processus de génération d'une JSR prometteuse n'est pas trivial. Il existe quelques voies par lesquelles les idées sont canalisées pour devenir des JSR. La plus importante d'entre elles est le JEP. Un grand nombre des changements les plus ambitieux apportés à Java (comme les lambdas) sont issus des JEP. Le processus de livraison d'un nouveau JDK au monde est en soi une JEP.

Projets JDK

Lorsqu'un effort est suffisamment large, il est considéré comme un projet JDK. Ce terme recouvre un large éventail d'artefacts, de la documentation au code, incluant souvent un ou plusieurs JEP. Les projets impliquent un ou plusieurs groupes de travail. Les groupes sont dédiés à divers domaines de la plateforme Java. Un projet compte généralement plusieurs personnes actives dans le rôle d'auteur.

async-profiler

Ce projet est un profileur d'échantillonnage à faible coût pour Java qui ne souffre pas du problème de biais de Safepoint. Il dispose d'APIs spécifiques à HotSpot pour collecter les traces de pile et pour suivre les allocations de mémoire. Le profileur fonctionne avec OpenJDK, Oracle JDK et d'autres runtimes Java basés sur la JVM HotSpot. async-profiler peut tracer les types d'événements suivants :

  • Cycles CPU ;
  • Compteurs de performance matériels et logiciels comme les manques de cache, les manques de branche, les défauts de page, les commutations de contexte ;
  • Allocations dans le tas de Java ;
  • Tentatives de verrouillages contenus, y compris les moniteurs d'objets Java et les ReentrantLocks.

Profilage du CPU

Dans ce mode, le profileur collecte des échantillons de trace de pile qui incluent des méthodes Java, des appels natifs, du code JVM et des fonctions du noyau. L'approche générale consiste à recevoir les piles d'appels générées par perf_events et à les faire correspondre aux piles d'appels générées par AsyncGetCallTrace, afin de produire un profil précis du code Java et natif. De plus, async-profiler fournit une solution de contournement pour récupérer les traces de pile dans certains cas où AsyncGetCallTrace échoue.

Cette approche présente les avantages suivants par rapport à l'utilisation directe de perf_events avec un agent Java qui traduit les adresses en noms de méthodes Java :

  • fonctionne sur les anciennes versions de Java car il ne nécessite pas -XX:+PreserveFramePointer, qui n'est disponible qu'à partir du JDK 8u60 ;
  • n'introduit pas le surcoût de performance de -XX:+PreserveFramePointer, qui peut dans de rares cas atteindre 10 % ;
  • ne nécessite pas la génération d'un fichier map pour faire correspondre les adresses de code Java aux noms de méthodes ;
  • fonctionne avec les cadres de l'interpréteur ;
  • ne nécessite pas l'écriture d'un fichier perf.data pour un traitement ultérieur dans les scripts de l'espace utilisateur.

Profilage de l'ALLOCATION

Au lieu de détecter le code consommateur de CPU, le profileur peut être configuré pour collecter les sites d'appel où la plus grande quantité de mémoire de tas est allouée. async-profiler n'utilise pas de techniques intrusives comme l'instrumentation du bytecode ou les sondes DTrace coûteuses qui ont un impact significatif sur les performances. Il n'affecte pas non plus l'analyse d'échappement et n'empêche pas les optimisations JIT comme l'élimination des allocations. Seules les allocations réelles du tas sont mesurées.

Le profileur présente un échantillonnage piloté par TLAB. Il s'appuie sur des callbacks spécifiques à HotSpot pour recevoir deux types de notifications :

  • lorsqu'un objet est alloué dans un TLAB nouvellement créé ;
  • lorsqu'un objet est alloué sur un chemin lent en dehors du TLAB.

Cela signifie que chaque allocation n'est pas comptée, mais seulement tous les N kB allocations, où N est la taille moyenne de TLAB. Cela rend l'échantillonnage du tas très bon marché et adapté à la production. D'un autre côté, les données collectées peuvent être incomplètes, bien qu'en pratique elles reflètent souvent les sources d'allocation les plus importantes.

L'intervalle d'échantillonnage peut être ajusté avec l'option --alloc. Par exemple, [/C]--alloc 500k[/C] prendra un échantillon après 500 Ko d'espace alloué en moyenne. Cependant, les intervalles inférieurs à la taille du TLAB ne seront pas pris en compte.

Sources : Java (1, 2)

Et vous ?

Quel est votre avis sur le sujet ?

Voir aussi :

Développeurs Java : la version d'OpenJDK de Microsoft est désormais disponible et inclut des binaires pour Java 11, basés sur OpenJDK 11.0.11

Le JDK 16 est déjà en cours de développement, bien que les travaux sur le JDK 15 continuent, et devrait arriver avec la prise en charge des fonctionnalités du C++ 14

La version open source du kit de développement Java (OpenJDK) débarque sur Windows 10 pour l'architecture ARM 64 bits, au travers d'un projet de portage initié par Microsoft