Geschiedenis van de vraag
Voor Java 8 update 20 moesten ontwikkelaars die de heapconsumptie van dubbele String instanties wilden verminderen, uitsluitend vertrouwen op String.intern(). Deze methode plaatste strings in de permanente generatie (later Metaspace), wat expliciete API-aanroepen vereiste en eventueel geheugen drukte in de intern pool veroorzaakte. Met JEP 192 introduceerde de G1 garbage collector automatische String Deduplication, een transparante optimalisatie die gericht is op het wijdverspreide probleem van overbodige karakterarrays in bedrijfsapplicaties.
Het probleem
In data-intensieve Java-toepassingen—zoals die welke XML, JSON of database-resultsets parseren—bestaan String objecten vaak voor 25-50% uit de live heap. Een aanzienlijk deel van deze strings is karakter-voor-karakter identiek maar bevindt zich in verschillende char[] (of byte[] na Java 9 Compact Strings) backing arrays. Zonder ingrijpen verspillen deze dubbele arrays geheugen en verhogen ze de GC frequentie. De uitdaging was om deze redundantie te elimineren zonder extra stop-the-world pauzes in te voeren of code wijzigingen te vereisen.
De oplossing
G1 voert deduplicatie opportunistisch uit tijdens zijn bestaande evacuatiepauze (wanneer threads al gestopt zijn). Wanneer ingeschakeld via -XX:+UseStringDeduplication, scant de collector objecten in de jonge generatie. Voor elke String die ten minste -XX:StringDeduplicationAgeThreshold garbage collections heeft overleefd (standaard 3), berekent G1 een hash van zijn backing array. Vervolgens raadpleegt het een deduplicatietabel. Als er een identieke array bestaat, gebruikt G1 een compare-and-swap (CAS) operatie om het value veld van de String naar de bestaande array te verwijzen, waardoor de duplicaat in de volgende cyclus kan worden teruggenomen. Dit benut de bestaande pauze en neemt alleen marginale CPU overhead met zich mee.
// Geen code wijzigingen vereist; JVM-vlaggen schakelen de optimalisatie in: // -XX:+UseG1GC -XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3 public class DeduplicationExample { public static void main(String[] args) { // Deze twee strings delen dezelfde backing array na deduplicatie String a = new String("FinancialInstrument".toCharArray()); String b = new String("FinancialInstrument".toCharArray()); // Na voldoende GC-cycli en evacuatiepauzes, // a.value == b.value (interne array referentie gelijkheid) } }
Een high-frequency tradingplatform dat FIX-protocolberichten verwerkt, ervoer ernstige G1 pauzetijden die 200ms overstegen. Profilering onthulde dat 30% van de 64GB heap werd verbruikt door String objecten die standaard tags vertegenwoordigden (bijv. "55", "150", "EUR/USD") en enum-achtige waarden die uit inkomende byte streams werden geparsed. Elke berichtinstantie creëerde nieuwe String instanties via new String(byte[], Charset), wat resulteerde in miljoenen dubbele backing arrays per minuut.
Verschillende oplossingen werden geëvalueerd. String.intern() werd afgewezen omdat het ingrijpende wijzigingen vereiste in meer dan 50 berichttypen en het risico met zich meebracht de Metaspace te verzadigen met permanente verwijzingen die nooit zouden worden geclaimd door de garbage collector. Een op WeakHashMap gebaseerde cache werd geprototyperd, maar introduceerde complexe gelijktijdigheidskosten en logica voor het opruimen van verouderde items die paradoxaal genoeg de GC druk verhoogden vanwege de extra WeakReference verwerking.
Het team schakelde uiteindelijk G1 String Deduplication in met de standaard leeftijdsdrempel van 3. Deze transparante aanpak vereiste geen code veranderingen en opereerde tijdens bestaande evacuatiepauzes, zonder nieuwe stop-the-world fasen.
Het resultaat was een 22% verlaging van het geheugengebruik en een daling van de 95e-percentiel pauzetijden tot onder de 50ms. De gemeten CPU overhead was ongeveer 1,5% tijdens piekuren van de markt, een aanvaardbare ruil voor de besparingen op geheugen en de verbetering van de latentie.
Hoe werkt String deduplicatie samen met Java 9's Compact Strings, die LATIN-1 tekst opslaan als byte[] in plaats van char[]?
Antwoord. String Deduplication werd bijgewerkt om te werken op byte[] arrays wanneer Compact Strings is ingeschakeld (de standaard sinds Java 9). De deduplicatielogica inspecteert het coder veld (LATIN1 of UTF16) en hasht de bijbehorende byte[] of char[] backing array dienovereenkomstig. De deduplicatietabel slaat items op die zijn gekoppeld aan zowel hash als arraytype, zodat Latin-1 strings worden gededupliceerd tegen andere Latin-1 strings, en full-width UTF-16 strings tegen hun soortgenoten. Kandidaten geloven vaak ten onrechte dat de functie is gedepruleerd met Compact Strings, maar het blijft volledig compatibel.
Waarom legt de JVM een leeftijdsdrempel op (standaard 3 GCs) voordat een String in aanmerking komt voor deduplicatie?
Antwoord. De leeftijdsdrempel voorkomt dat het systeem CPU-cycli verspilt met het dedupliceren van kortlevende, ephemere strings die waarschijnlijk in de volgende jonge collectie zullen sterven. Door te vereisen dat de String verschillende G1 evacuatiecycli overleeft (het bevorderen van Eden naar Survivor regio's en uiteindelijk naar Tenured), zorgt de heuristiek ervoor dat alleen "volwassen" strings—die een hoge waarschijnlijkheid van lange termijn overleving hebben—worden verwerkt. Dit amortiseert de kosten van de hash berekening en tabellookup over de verwachte levensduur van het object.
Beïnvloedt String deduplicatie de onveranderlijkheid of hashCode stabiliteit van de String instantie?
Antwoord. Nee. Het deduplicatieproces is strikt een implementatiedetail van de verwijzing van het value veld. Aangezien de vervangende array identieke bytes of karakters bevat, blijft de logische staat van de String en de hashCode ongewijzigd. De hashCode wordt opgeslagen in een vluchtig veld binnen het String object zelf, en omdat de inhoud identiek is, blijft de opgeslagen waarde geldig. Het equals contract wordt behouden omdat inhoud gelijkheid impliceert dat referentie gelijkheid van de backing store irrelevant is voor het API-contract. De operatie is atomair vanuit het perspectief van de applicatie, behoudend String's onveranderlijke garantie.