JavaProgrammationDéveloppeur Java Senior

Par quel optimizatio spécifique le **G1** consolide-t-il de manière transparente les tableaux de soutien **String** dupliqués lors des cycles de collecte des ordures sans prolonger les durées d'arrêt du monde?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question.

Historique de la question

Avant Java 8 mise à jour 20, les développeurs cherchant à réduire la consommation de tas due aux instances String dupliquées devaient s'appuyer exclusivement sur String.intern(). Cette méthode plaçait les chaînes dans la génération permanente (plus tard Metaspace), nécessitant des appels d'API explicites et pouvant causer une pression mémoire dans le pool d'internement. Avec JEP 192, le ramasse-miettes G1 a introduit la Déduplication de String automatique, une optimisation transparente ciblant le problème omniprésent des tableaux de caractères redondants dans les applications d'entreprise.

Le problème

Dans les applications Java intensives en données—comme celles analysant XML, JSON ou les ensembles de résultats de bases de données—les objets String représentent souvent 25 à 50 % du tas vivant. Une partie significative de ces chaînes est identique caractère par caractère mais réside dans des tableaux de soutien distincts char[] (ou byte[] après Java 9 Compact Strings). Sans intervention, ces tableaux dupliqués gaspillent de la mémoire et augmentent la fréquence de la collecte des ordures. Le défi était d'éliminer cette redondance sans introduire de pauses d'arrêt du monde supplémentaires ni nécessiter de modifications de code.

La solution

G1 effectue la déduplication de manière opportuniste durant sa pause d'évacuation existante (quand les fils sont déjà arrêtés). Lorsqu'elle est activée via -XX:+UseStringDeduplication, le ramasse-miettes scanne les objets dans la jeune génération. Pour chaque String qui a survécu à au moins -XX:StringDeduplicationAgeThreshold collectes de déchets (par défaut 3), G1 calcule un hachage de son tableau de soutien. Il consulte ensuite une table de déduplication. Si un tableau identique existe, G1 utilise une opération compare-and-swap (CAS) pour rediriger le champ value de la String vers le tableau existant, permettant à la duplication d'être récupérée lors du cycle suivant. Cela exploite la pause existante, n'ajoutant qu'une surcharge CPU marginale.

// Aucun changement de code requis; les flags JVM activent l'optimisation: // -XX:+UseG1GC -XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3 public class DeduplicationExample { public static void main(String[] args) { // Ces deux chaînes partagent le même tableau de soutien après déduplication String a = new String("FinancialInstrument".toCharArray()); String b = new String("FinancialInstrument".toCharArray()); // Après un nombre suffisant de cycles de GC et de pauses d'évacuation, // a.value == b.value (égalité de référence de tableau interne) } }

Situation de la vie réelle

Une plateforme de trading haute fréquence traitant des messages du protocole FIX a connu de graves temps de pause G1 dépassant 200 ms. Le profilage a révélé que 30 % du tas de 64 Go était consommé par des objets String représentant des balises standards (par exemple, "55", "150", "EUR/USD") et des valeurs de type énuméré analysées à partir de flux d'octets entrants. Chaque instantiation de message créait de nouvelles instances String via new String(byte[], Charset), entraînant des millions de tableaux de soutien dupliqués par minute.

Plusieurs solutions ont été évaluées. String.intern() a été rejeté car il nécessitait des modifications invasives dans plus de 50 types de messages et risquait de saturer le Metaspace avec des références permanentes qui ne seraient jamais collectées par les ordures. Un cache basé sur WeakHashMap a été prototypé, mais a introduit une surcharge de concurrence complexe et une logique de nettoyage des entrées périmées qui augmentait paradoxalement la pression GC en raison du traitement supplémentaire des WeakReference.

L'équipe a finalement activé la Déduplication de String de G1 avec le seuil d'âge par défaut de 3. Cette approche transparente n'a nécessité aucun changement de code et a fonctionné pendant les pauses d'évacuation existantes, évitant toute nouvelle phase d'arrêt du monde.

Le résultat a été une réduction de 22 % de l'utilisation du tas et une baisse des temps de pause au 95ème percentile à moins de 50 ms. La surcharge CPU mesurée était d'environ 1,5 % pendant les heures de pointe du marché, un compromis acceptable pour les économies de mémoire et l'amélioration de la latence.

Ce que les candidats manquent souvent

Comment la déduplication des chaînes interagit-elle avec les Compact Strings de Java 9, qui stockent du texte Latin-1 sous forme de byte[] au lieu de char[]?

Réponse. La Déduplication de String a été mise à jour pour fonctionner sur des tableaux byte[] lorsque les Compact Strings sont activées (le défaut depuis Java 9). La logique de déduplication inspecte le champ coder (LATIN1 ou UTF16) et hache le tableau de soutien correspondant byte[] ou char[] en conséquence. La table de déduplication stocke les entrées indexées par hachage et type de tableau, garantissant que les chaînes Latin-1 sont dédupliquées par rapport à d'autres chaînes Latin-1, et les chaînes UTF-16 à largeur complète par rapport à leurs pairs. Les candidats croient à tort que la fonctionnalité a été déposée avec les Compact Strings, mais elle reste entièrement compatible.

Pourquoi la JVM impose-t-elle un seuil d'âge (par défaut 3 GCs) avant qu'une chaîne ne devienne éligible à la déduplication?

Réponse. Le seuil d'âge empêche le système de gaspiller des cycles CPU à dédupliquer des chaînes éphémères à courte durée de vie qui vont probablement mourir lors de la prochaine collecte jeune. En exigeant que la String survive à plusieurs cycles d'évacuation G1 (la promouvant de régions Eden à Survivor et finalement vers Tenured), l'heuristique garantit que seules les chaînes "matures"—celles ayant une forte probabilité de survie à long terme—sont traitées. Cela amortit le coût du calcul de hachage et de la recherche dans la table sur la durée de vie attendue de l'objet.

La déduplication des chaînes affecte-t-elle l'immuabilité ou la stabilité de hashCode de l'instance String?

Réponse. Non. Le processus de déduplication est strictement un détail d'implémentation de la mutation de référence du champ value. Puisque le tableau de remplacement contient des octets ou caractères identiques, l'état logique de la String et son hashCode restent inchangés. Le hashCode est mis en cache dans un champ transitoire au sein de l'objet String lui-même, et puisque le contenu est identique, la valeur mise en cache reste valide. Le contrat equals est préservé car l'égalité du contenu implique que l'égalité de référence du magasin de soutien est sans importance pour le contrat de l'API. L'opération est atomique du point de vue de l'application, maintenant la garantie d'immuabilité de la String.