Avec l’arrivée d’une montée en charge, ou face à une volumétrie croissante, les applications Java peuvent rencontrer des problèmes de performance. A ce moment se pose la question de l’optimisation. Comment optimiser efficacement ?
Cet article est la suite de l’article « Optimiser une application JAVA – Partie 1« .
Il présente les différents étapes nécessaires à une optimisation efficace d’une application JAVA.
Pour rappel :
Les étapes de l’optimisation
Une optimisation se réalise de manière cyclique selon les étapes suivantes :
-
Identifier les processus les plus pénalisants
-
Cibler et réaliser une optimisation
-
Vérifier le gain obtenu
-
Si le gain obtenu est satisfaisant, l’optimisation est terminée. Sinon retour à l’étape 1, en cherchant le nouvel élément le plus pénalisant.
1. Identifier les processus pénalisants
Sans cette étape, il est impossible de réaliser une optimisation efficace. Elle nous permet de focaliser les travaux d’optimisation sur les points les plus bloquants. L’utilisation d’un profiler facilite leur recherche. Il existe de nombreux profiler p our J ava . Les captures d’écran ci-dessous illustre des résultats obtenus avec Your Kit Java Profiler (YJP) , un profiler dont l’utilisation et la mise en place est simple et rapide.
Les sources de lenteurs d’une application peuvent avoir de nombreuses origines. Les plus fréquemment rencontrées sont :
-
une utilisation CPU trop importante
-
une saturation de la mémoire
-
les contentions de threads
-
un nombre trop important de requêtes SQL provoquant des latences à cause des appels réseaux
-
ou bien encore des libérations massives d’objets volumineux, provoquant un pique d’activité pour le Garbage Collector.
Le profiler nous permet de mesurer l’activité de tous ces éléments, comme l’illustre les capture d’écran ci-dessous :
-
Les méthodes les plus consommatrices en CPU
-
Les méthodes les plus consommatrices en mémoire
-
Les contentions de threads
-
Les requêtes SQL utilisées, avec leurs temps d’exécution et leurs nombres d’invocations
-
L’activité du Garbage Collector
2. Cibler et réaliser des optimisations
Les résultats du profilage nous permettent maintenant de connaître les méthodes les plus critiques. L’optimisation devra donc se concentrer sur ces méthodes afin de garantir un gain significatif. Il nous reste donc à déterminer quoi faire. Selon les problématiques rencontrées, il existe plusieurs solutions pour améliorer les temps d’exécution. Ci-dessous, une liste présente des problèmes fréquents.
Activité trop importante du CPU
Les pics de CPU sont souvent provoqués par des boucles ou des méthodes dont les traitements durent longtemps. Une solution peut être de découper le traitement :
-
le traitement peut-il être réalisé en plusieurs étapes ?
-
Le traitement peut-il être adapter à un algorithme multithreadé ?
Mémoire saturée
La saturation de la mémoire peut être provoquée par un traitement qui charge en mémoire un nombre trop important de données. Par exemple, une méthode qui crée une liste de tout le contenu d’une table, alors que celle-ci contient plusieurs dizaines de milliers de tuples. Dans un cas comme celui-ci, la solution est soit de récupérer les données par lots de données, ou d’utiliser un curseur pour traiter les données une à une sans charger toute la table d’un coup.
Contention de threads
Une contention de threads provient d’un ensemble de threads qui se retrouvent dans l’état bloqué. Si ce blocage durent trop longtemps, l’application peut subir des latences. Les blocages peuvent être induit à plusieurs niveaux : soit au niveau java à cause de l’utilisation de synchronize, soit au niveau de la base de données à cause de deadlocks. Dans cette situation, la solution provient souvent de l’amélioration de l’algorithme afin de contourner l’utilisation des verrous, ou d’en réduire au maximum l’utilisation.
Requêtes SQL pénalisantes
Les requêtes SQL peuvent être à l’origine de plusieurs problèmes.
Des temps de réponses de requêtes trop longs
Une requête SQL qui met trop longtemps à répondre, par exemple, demandera des optimisations au niveau de la base de données. Ce genre de problème se résous en analysant le plan d’exécution ( EXPLAIN PLAN) de la requête concernée.
Un nombre très important de requêtes
Lorsqu’une application provoque un très grand nombre de requêtes SQL, elle est souvent victime de latence à cause du nombre d’appels répétés à la base de données. Dans cette situation, la solution dépend de l’usage des requêtes SQL.
De nombreuses requêtes SELECT identiques
Un nombre important de requêtes de type SELECT identique peut être résolu par l’utilisation de recherche en lot en utilisant une clause IN. Par exemple, un traitement qui engendre un grand nombre de requêtes du genre suivant :
SELECT * FROM PRODUCT WHERE PRODUCT_ID = ?
Si le traitement le permet, il sera plus efficace de traiter les produits par lots, et de les rechercher en une seule requête :
SELECT * FROM PRODUCT WHERE PRODUCT_ID IN (?, … ,?)
De nombreuses requêtes SELECT hétérogènes
Par ailleurs, le problème provient peut être d’un nombre important de requêtes hétérogènes. Si les requêtes sont toutes de types SELECT et qu’elles proviennent du même processus, alors les données sont peut être liées. Dans ce cas, il est préférables d’utiliser des jointures pour rechercher l’ensemble des données en une seule requête plutôt que de les rechercher indépendamment. Par exemple, un traitement qui recherche les prix d’un ensemble de produits, en effectuant une première recherche pour trouver les informations sur les produits puis leurs prix :
SELECT * FROM PRODUCT WHERE PRODUCT_ID IN (?, … ,?) SELECT * FROM PRODUCT_PRICE WHERE PRODUCT_ID IN (?, … ,?)
Il sera plus performant de rechercher les données en combinant les deux requêtes :
SELECT * FROM PRODUCT P INNER JOIN PRODUCT_PRICE PP ON PP.PRODUCT_ID = P.PRODUCT_ID WHERE P.PRODUCT_ID IN (?, … ,?)
De nombreuses requêtes hétérogènes et de tous types
Dans des traitements batches par exemple, on peut se retrouver dans une situation qui requière de très nombreuses opérations de mises à jour. Dans une configuration comme celle-ci, l’utilisation du mode JDBC batch-update permet d’améliorer très significativement les performances, en envoyant en une seule fois l’ensemble des opérations à effectuer à la base de données. Deux modes sont disponibles qui répondent à des usages différents :
- Mode PREPARED_STATEMENT : on souhaite envoyer un nombre important de mises à jour mais toujours avec la même requête, seul les paramètres de cette requête changent.
Exemple : On souhaite mettre à jour les noms de produits d’une liste de produitsPreparedStatement ps = connection.prepareStatement( "UPDATE PRODUCT SET INTERNAL_NAME = ? WHERE PRODUCT_ID = ?"); ps.setString(1, "Produit1"); ps.setString(2, "0001"); ps.addBatch(); ps.setString(1, "Produit2"); ps.setString(2, "0002"); ps.addBatch(); ps.executeBatch();
- Mode STATEMENT : on souhaite envoyer un grand nombre de requêtes toutes différentes UPDATE et/ou DELETE mélangés avec des paramètres, des opérations et des clauses différentes.
Exemple : On souhaite mettre à jour un produit, supprimer un autre et changer un prix, toutes les requêtes seront envoyés en un seul appel bien qu’elles soient toutes différentes.Statement st = connection.createStatement() ; st.addBatch( "UPDATE PRODUCT SET INTERNAL_NAME = 'TUTU' WHERE PRODUCT_ID = '1'"); st.addBatch("DELETE FROM PRODUCT WHERE PRODUCT_ID = '3'"); st.addBatch( "UPDATE PRODUCT_PRICE SET PRICE = 29.99 WHERE PRODUCT_ID = '5'"); st.executeBatch();3. Valider l’efficacité des optimisations réalisées
La dernière étape consiste à profiler à nouveau l’application pour mesurer le gain obtenu. Si l’optimisation réalisée est efficace, le partie optimisée ne devrait plus apparaître comme critique.
Ensuite, il reste à réitérer les étapes de l’optimisation sur les points critiques suivants jusqu’à obtenir une optimisation globale satisfaisante.





