Java : comment éviter la surconsommation de ressources CPU
Déterminer pourquoi une application Java s’accapare trop de ressources CPU n’est pas toujours simple. Passons en revue quelques erreurs courantes et examinons ce qui provoque ces défauts sous le capot.
Un problème de performance d’un runtime Java émane généralement d’un rapport d’utilisation élevée du processeur depuis un profiler JVM ou d’un outil de monitoring. Malheureusement, la cause de ces pics de consommation sous Windows et Linux n’est parfois pas facile à identifier, car cette mesure génère souvent une fausse piste pour une complication périphérique au code.
Par exemple, si une application alloue les instances avec trop de zèle, le collecteur de déchets (Garbage Collector – GC) sera contraint d’intervenir lorsque les références d’objets ne sont plus accessibles. Des cycles GC de plus en plus fréquents déclencheront non seulement des événements stop-the-world au niveau de la JVM (machine virtuelle Java) – qui donneront l’impression qu’une application ne répond pas –, mais ils provoqueront également une sur sollicitation du processeur. Ici, il ne s’agit pas de la mise en œuvre d’algorithmes plus efficaces ou de workflows logiques. La solution consiste à résoudre le problème sous-jacent d’allocation d’objets qui emploient inutilement de la mémoire et engendre des opérations de collecte de déchets superflues.
Les métriques en provenance du CPU peuvent être trompeuses
Les threads bloqués par des conflits peuvent également amener les outils de profilage JVM à signaler une consommation à 100 % du processeur par une application Java. Les problèmes liés à la concurrence et les blocages ne sont pas vraiment du fait du CPU, mais plutôt à une mauvaise allocation des threads et de synchronisation ou de blocage des méthodes auxquelles ils accèdent.
L’usage de l’unité centrale peut aussi être une mesure trompeuse.
Lorsqu’un CPU est au repos, les solutions de monitoring rapportent qu’il est inutilisé, comme s’il était à l’arrêt. Cependant, lorsque les threads sont bloqués, ils mettent les instructions en attente. L’UC n’exécute aucune instruction logique, mais elle signale aux outils de profilage de la JVM qu’elle est occupée, malgré le fait qu’elle ne fasse rien.
De plus, ce n’est pas parce que toutes vos ressources CPU sont utilisées que la JVM en est la cause. Cette consommation grimpe lorsque votre application monte en charge, mais ce pic peut être imputable à un processus système ou à une mauvaise configuration de la stack logicielle. Si la mémoire virtuelle d’un serveur est mal paramétrée, la destruction des fichiers page dépensera la majorité des cycles. Un problème de mémoire virtuelle doit être résolu par l’équipe DevOps ou les administrateurs système. Ce problème n’est pas attribuable à votre application ou à la façon dont vous avez réglé les performances de la JVM.
Les problèmes de performances Java les plus courants
La majorité des problèmes de performance de la JVM peuvent être attribués aux opérations I/O telles que les écritures dans le répertoire de fichiers ou les interactions avec un SGBDR en arrière-plan ou une file d’attente de messages (message queue). Un pool de connexion de base de données mal configuré, où des ressources sont constamment créées et détruites pour fournir un lien continu entre la base de données Java aux services Spring et JPA, peut provoquer cette surconsommation. Une mauvaise gestion des processus I/O peut également entraîner des fuites de mémoire et une inévitable OutOfMemoryError.
Une autre cause à ce même phénomène peut être du fait d’une API RESTful mal programmée qui passe trop d’appels réseau vers des composants tiers. Les applications « bavardes » comportant un grand nombre de requêtes HTTP, ainsi que l’analyse JSON et XML associés à chaque cycle de demande-réponse, déclenchent souvent des rapports d’utilisation du processeur à 100 %. Ce problème est devenu de plus en plus courant dans les architectures d’entreprise modernes, car les développeurs réorganisent les monolithes logiciels en microservices.
Les causes indirectes de la forte utilisation du processeur avec Java
Lorsque vous résolvez des problèmes d’utilisation élevée du CPU avec une application Java, la première étape consiste à éliminer les diverses fausses pistes mentionnées ci-dessus. Pour rappel, ces problèmes périphériques sont les suivants :
- une mauvaise gestion de la mémoire JVM ;
- une mauvaise configuration du collecteur de déchets Java ;
- des problèmes plus correctement imputables à la stack logicielle ;
- les problèmes de synchronisation des threads, de contention et de deadlocks
- les problèmes I/O de fichiers et de bases de données sous-jacents.
Ce n’est qu’une fois avoir terminé l’analyse des potentielles causes profondes listées ci-dessus qu’il faudra prendre le temps d’ausculter et de corriger le code.
Débutez avec Java Flight Recorder.
Les causes directes d’un usage CPU élevé avec Java
Quel pourrait être le coupable lorsque votre code Java sollicite trop le processeur ? Les causes les plus courantes et les plus directement imputables aux problèmes d’utilisation élevée du CPU sont les suivantes :
- la création de boucles infinies, par inadvertance
- des workflows mal conçus et des algorithmes inefficaces
- l’utilisation de la logique récursive
- des classes de collection mal choisies ; et
- le recalcul des valeurs déjà calculées.
Boucles infinies
Qu’il s’agisse d’une erreur logique ou d’un simple développement bâclé, il n’est pas rare qu’un programmeur démarre une boucle et code incorrectement l’état qui en résulte. Cela déclenche une boucle infinie qui ne fait rien d’autre que consommer des fréquences d’horloge. Si plusieurs threads touchent cette ligne de code, vous avez une application multithreads qui ne produit que des itérations insignifiantes. Éliminez les boucles infinies et l’utilisation du CPU devrait revenir à la normale.
Flux de travail et algorithmes mal codés
Le CPU exécute la logique. Si une application comporte des flux de travail mal écrits et que le code ressemble à une assiette de spaghettis, votre CPU dévorera des cycles d’horloge inutiles. Mettez à jour les flux de travail couramment invoqués et retravaillez les algorithmes peu performants pour tirer le meilleur parti de votre CPU.
Logique récursive
Si certains langages de programmation sont optimisés pour la logique récursive, Java n’en fait pas partie. Les algorithmes récursifs créent des threads difficiles à interrompre, attribuent des objets qui ne sont pas facilement récupérés par les agents de collecte des déchets et forment des stack frames complexes à dérouler. Ajoutez à cela la menace imminente d’une StackOverflowError, et l’emploi d’algorithmes itératifs se justifie amplement.
Des classes Collections mal choisies
Le traitement des listes est au cœur de la plupart des applications d’entreprise. En tant que tels, les développeurs ont le choix entre de nombreuses classes Collections. Si un développeur emploie une LinkedList au lieu d’une ArrayList sur un grand ensemble de données, le CPU sera fortement sollicité. De même, si un programmeur recourt à l’ancienne Hashtable plutôt qu’une HashMap, la synchronisation peut consommer vainement des ressources. Choisissez la mauvaise classe de collection Java, et les performances de l’application en souffriront. Sélectionnez les bonnes et vos problèmes disparaîtront.
Recalcul des valeurs déjà traitées
Il n’est pas rare qu’une valeur donnée soit calculée de nombreuses fois au cours d’une application. Si c’est le cas, conservez le résultat du premier calcul dans une variable et faites référence à cette variable pour toutes les interactions futures. De petites modifications de ce type peuvent avoir un impact significatif sur les performances de l’application, surtout s’il s’agit de cryptographie, de manipulation graphique ou d’autres opérations gourmandes en CPU.
Avec un bon profiler JVM comme Java Flight Recorder, et un outil d’analytique – JDK Mission Control par exemple – pour inspecter les métriques, l’identification du coupable responsable des problèmes de forte utilisation du processeur ne devrait pas poser de difficulté. Et une fois cerné, il suffit de mettre en place de nouvelles routines logicielles et de tester les résultats jusqu’à ce que le correctif soit déployé.