Java String Pool
Wie der Java String Pool funktioniert, warum String-Literale interniert werden und was die intern()-Methode bewirkt.
Ein typisches Java-Programm erstellt Tausende von Strings, und ein großer Teil davon enthält dieselben Zeichen wie ein anderer String an einer anderen Stelle im Programm. Methodennamen. Konfigurationsschlüssel. Fehlermeldungen. Feldbezeichnungen. Die JVM betrachtet diese Redundanz als lösungswürdig — sie hält einen speziellen Bereich namens String Pool (oder String Intern Table) vor und weist jedem Literal, das im Quellcode vorkommt, einen gemeinsamen Eintrag darin zu. Zwei Literale mit denselben Zeichen verweisen am Ende auf dasselbe Objekt.
Dieses Teilen hat sichtbare Konsequenzen für den Identitätsvergleich (==), die Speichernutzung und eine kleine Anzahl subtiler Fehler rund um intern(). Dieses Kapitel beschreibt die Regeln.
Literale werden gepoolter, new String(...) nicht
Die wichtigste Grundregel:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — same pooled object
String c = new String("hello");
System.out.println(a == c); // false — new String, fresh objectJedes String-Literal, das der Compiler erkennt, wird beim ersten Laden in den Pool aufgenommen. Nachfolgende Vorkommen desselben Literals — überall im Programm, in jeder Klasse — geben dieselbe Referenz zurück. Daher sind a und b dasselbe Objekt.
new String("hello") erzwingt eine neue Allokation auf dem Heap. Das Argument "hello" wird weiterhin gepoolert (weil es ein Literal ist), aber der Konstruktor kopiert es in ein neues Objekt außerhalb des Pools. c und a haben daher zwar gleiche Inhalte, aber unterschiedliche Identitäten.
Das ist der eigentliche Grund, warum in jedem Java-Lehrbuch "use equals, not ==" eingedrückt wird. Der Identitätsvergleich funktioniert zufällig bei einfachen Literalen, schlägt aber fehl, sobald ein String aus new, aus einem Parser, aus Netzwerkeingaben oder aus einer Verkettung stammt, die der Compiler zur Kompilierzeit nicht falten konnte.
Was sich im Pool befindet
Der Pool wird auf zwei Wegen befüllt:
- String-Literale im Quellcode. Der Compiler gibt jedes eindeutige Literal als
CONSTANT_String-Eintrag im Konstantenpool der Klasse aus; die JVM löst es beim ersten Verwenden der Klasse in ein echtesString-Objekt im heap-residenten Pool auf. - Explizite
intern()-Aufrufe. JederString, auf den Sie eine Referenz haben, kann durch den Aufruf vons.intern()in den Pool aufgenommen werden. Die Methode gibt die gepoolerte Instanz zurück — das ist für jeden Aufrufer, der gleiche Inhalte interniert, dieselbe Referenz.
Berechnete Strings — a + b, s.substring(...), Ergebnisse von String.format — werden nicht automatisch gepoolert. Sie leben dort, wo der GC sie abgelegt hat, und haben die Identität, die sie zufällig haben.
String x = "java";
String y = "ja" + "va"; // compile-time constant — pooled, == x
String z = "ja" + new String("va"); // runtime computation — NOT pooled
System.out.println(x == y); // true
System.out.println(x == z); // false
System.out.println(x == z.intern()); // true — intern() returns the pooled instanceDer zweite Fall ist die Falle. y wird aus zwei Literalen berechnet, aber der Compiler faltet die Verkettung zur Kompilierzeit, sodass das Ergebnis einfach ein weiteres Literal ist — gepoolert. Bei z ist ein new zur Laufzeit beteiligt, der Compiler kann es nicht falten, und das resultierende Objekt lebt außerhalb des Pools.
Die intern()-Methode
String#intern() macht in einem Aufruf zwei Dinge:
- Wenn ein String mit denselben Zeichen bereits im Pool vorhanden ist, wird diese gepoolerte Referenz zurückgegeben.
- Andernfalls wird dieser String zum Pool hinzugefügt und zurückgegeben.
Das zweite Verhalten ist das nützliche, wenn Sie Strings zur Laufzeit aus einem kleinen, aber häufig verwendeten Vokabular erstellen — HTTP-Header-Namen, die aus Bytes geparst werden, Tokens aus einem Lexer, Spaltennamen, die aus einem Datenbanktreiber gelesen werden. Das Internieren faltet N separate Objekte zu einem und bedeutet, dass nachgelagerte Vergleiche == verwenden können, wenn Sie gemessen haben, dass es den Aufwand wert ist.
String s1 = new String("status").intern();
String s2 = new String("status").intern();
System.out.println(s1 == s2); // true — both refer to the pooled "status"Der Haken: Jeder intern()-Aufruf kostet eine Hash-Suche, und gepoolerte Strings leben in einer Hash-Tabelle fester Größe, die sich nicht verkleinert. Wenn Sie unbegrenzte Eingaben internieren (vom Benutzer eingegebene Suchanfragen, Request-IDs), füllen Sie den Pool langsam mit Strings, die nie wiederverwendet werden — ein Speicherleck in Zeitlupe. Internieren Sie nur, wenn (a) die Menge der Werte begrenzt ist und (b) Sie ein messbares Problem haben, das es zu lösen gilt.
Interna des Pools (vereinfacht)
Der Pool ist als Hash-Tabelle innerhalb der JVM implementiert. In HotSpot ist es eine StringTable mit einer Standardkapazität, die im Laufe der Jahre erhöht wurde (derzeit 65.536 Buckets in den meisten Builds). Sie können ihn an der Kommandozeile untersuchen:
java -XX:+PrintStringTableStatistics MyAppFür Anwendungscode ist die Implementierung unsichtbar: Sie können nicht fragen "Ist dieser String im Pool?" über die öffentliche API, und das ist auch nicht nötig. Das sichtbare Verhalten ist == bei gleichen Literalen und intern() für das optionale Einbinden berechneter Strings.
Warum == für Strings trotzdem falsch ist
Der Pool kann dafür sorgen, dass == bei Testdaten zu funktionieren scheint:
String a = "hello";
String b = "hello";
if (a == b) { ... } // happens to be trueDann übergibt jemand den String durch BufferedReader.readLine() und == wird stillschweigend false. Der Vertrag, den Sie wollen, lautet "haben diese dieselben Zeichen?", und dieser Vertrag heißt a.equals(b). Der Pool ist eine Speicheroptimierung, keine Vergleichsstrategie — verlassen Sie sich nie darauf für Korrektheit.
Ein durchgearbeitetes Beispiel
Das folgende Beispiel macht das Verhalten des Pools sichtbar. Jeder printRef-Aufruf zeigt den System-Identitäts-Hash (ein einzeiliger Stellvertreter für "welches Objekt ist das?"), damit Sie sehen können, wo Literale den Speicher teilen und wo berechnete Strings das nicht tun.
Lesen Sie zuerst die Identitäts-Hashes: die Literale und die Faltung zur Kompilierzeit teilen denselben. runtimeConcat und fresh haben jeweils einen eigenen. interned stimmt wieder mit dem Literal überein, weil intern() die gepoolerte Instanz zurückgegeben hat, nicht die mit new allozierte. Die ==-Ergebnisse folgen direkt aus den Identitäten; equals gibt für alle true zurück, weil sie inhaltlich wirklich gleich sind.
Was kommt als Nächstes
Der Pool existiert, weil String unveränderlich ist — das Teilen desselben Objekts zwischen Aufrufern ist nur sicher, wenn niemand seinen Inhalt ändern kann. Das nächste Kapitel greift diesen Faden auf: warum Unveränderlichkeit gewählt wurde, was sie bringt und welchen Designkompromiss sie erzwingt. Weiter zu Java String immutability.