Strings beanspruchen in jeder Anwendung viel Speicher. Insbesondere char[]-Arrays, die einzelne UTF-16-Zeichen enthalten, tragen am meisten zum JVM-Speicherverbrauch bei – da jedes Zeichen 2 Bits belegt.
Tatsächlich kommt es sehr häufig vor, dass 30 % des Speichers von Strings belegt werden, nicht nur, weil Strings das beste Format für unsere Interaktion sind, sondern auch, weil die beliebte HTTP-API viele Strings verwendet. Mit Java 8 Update 20 haben wir jetzt Zugriff auf eine neue Funktion namens String-Deduplizierung, die den G1-Garbage Collector erfordert, der standardmäßig deaktiviert ist.
Die String-Deduplizierung nutzt die Tatsache, dass das Innere des Strings tatsächlich ein char-Array ist und endgültig ist, sodass die JVM sie beliebig manipulieren kann.
Für die String-Deduplizierung haben Entwickler eine große Anzahl von Strategien in Betracht gezogen, aber die endgültige Implementierung hat den folgenden Ansatz übernommen:
Immer wenn der Garbage Collector auf das String-Objekt zugreift, wird ein char-Array markiert. Es nimmt den Hash-Wert des char-Arrays und speichert ihn mit einem schwachen Verweis auf das Array. Solange der Garbage Collector eine andere Zeichenfolge findet, die denselben Hash-Code wie das char-Array hat, werden die beiden Zeichen für Zeichen verglichen.
Wenn sie übereinstimmen, wird eine Zeichenfolge so geändert, dass sie auf das char-Array der zweiten Zeichenfolge verweist. Das erste char-Array wird nicht mehr referenziert und kann recycelt werden.
Natürlich bringt dieser ganze Prozess etwas Overhead mit sich, aber er wird durch eine sehr enge Obergrenze gesteuert. Wenn beispielsweise festgestellt wird, dass sich ein Zeichen nicht wiederholt, wird es für einen bestimmten Zeitraum nicht mehr überprüft.
Wie funktioniert diese Funktion eigentlich? Zuerst benötigen Sie das gerade veröffentlichte Java 8 Update 20 und folgen dann dieser Konfiguration: -Xmx256m -XX:+UseG1GC, um den folgenden Code auszuführen:
public class LotsOfStrings { private static final LinkedList<String> LOTS_OF_STRINGS = new LinkedList<>(); public static void main(String[] args) throws Exception { int iteration = 0; while (true) { for (int i = 0; i < 100; i++) { for (int j = 0; j < 1000; j++) { LOTS_OF_STRINGS.add(new String("String " + j)); } } iteration++; System.out.println("Survived Iteration: " + iteration); Thread.sleep(100); } } }
Dieser Code führt 30 Iterationen aus und meldet einen OutOfMemoryError . .
Aktivieren Sie nun die String-Deduplizierung und verwenden Sie die folgende Konfiguration, um den obigen Code auszuführen:
-Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics
An diesem Punkt kann es länger laufen und nach 50 Iterationen beendet werden.
Die JVM gibt jetzt auch aus, was sie getan hat:
[GC concurrent-string-deduplication, 4658.2K->0.0B(4658.2K), avg 99.6%, 0.0165023 secs] [Last Exec: 0.0165023 secs, Idle: 0.0953764 secs, Blocked: 0/0.0000000 secs] [Inspected: 119538] [Skipped: 0( 0.0%)] [Hashed: 119538(100.0%)] [Known: 0( 0.0%)] [New: 119538(100.0%) 4658.2K] [Deduplicated: 119538(100.0%) 4658.2K(100.0%)] [Young: 372( 0.3%) 14.5K( 0.3%)] [Old: 119166( 99.7%) 4643.8K( 99.7%)] [Total Exec: 4/0.0802259 secs, Idle: 4/0.6491928 secs, Blocked: 0/0.0000000 secs] [Inspected: 557503] [Skipped: 0( 0.0%)] [Hashed: 556191( 99.8%)] [Known: 903( 0.2%)] [New: 556600( 99.8%) 21.2M] [Deduplicated: 554727( 99.7%) 21.1M( 99.6%)] [Young: 1101( 0.2%) 43.0K( 0.2%)] [Old: 553626( 99.8%) 21.1M( 99.8%)] [Table] [Memory Usage: 81.1K] [Size: 2048, Min: 1024, Max: 16777216] [Entries: 2776, Load: 135.5%, Cached: 0, Added: 2776, Removed: 0] [Resize Count: 1, Shrink Threshold: 1365(66.7%), Grow Threshold: 4096(200.0%)] [Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0] [Age Threshold: 3] [Queue] [Dropped: 0]
Der Einfachheit halber müssen wir die Summe aller Daten nicht selbst berechnen bequem zu verwenden. Die Summe reicht aus.
Das obige Code-Snippet legt fest, dass eine String-Deduplizierung durchgeführt wird und es 16 ms dauerte, um etwa 120.000 Strings anzuzeigen.
Die oben genannten Funktionen wurden gerade erst eingeführt, was bedeutet, dass sie möglicherweise noch nicht vollständig überprüft wurden. Die genauen Daten können in tatsächlichen Anwendungen anders aussehen, insbesondere in solchen, in denen Zeichenfolgen mehrmals verwendet und übergeben werden, sodass einige Zeichenfolgen möglicherweise übersprungen werden oder bereits Hashcodes enthalten (wie Sie vielleicht wissen, wird ein String-Hashcode träge geladen).
Im obigen Fall wurden alle Zeichenfolgen dedupliziert, wodurch 4,5 MB Daten im Speicher entfernt wurden.
Der Abschnitt [Tabelle] enthält Statistiken über die interne Tracking-Tabelle und die [Warteschlange] listet auf, wie viele Deduplizierungsanforderungen aufgrund der Auslastung verworfen wurden, was ebenfalls Teil des Overhead-Reduzierungsmechanismus ist.
Was ist also der Unterschied zwischen String-Deduplizierung und String-Persistenz? In meinem Blog gibt es einen Artikel mit dem Titel „Wie großartig String Interning für die Speichereffizienz ist“. Tatsächlich sehen String-Deduplizierung und Persistenz ähnlich aus, mit der Ausnahme, dass der Persistenzmechanismus die gesamte String-Instanz wiederverwendet, nicht nur das Zeichenarray.
Die Ersteller des JDK Enhancement Proposal 192 behaupten, dass Entwickler oft nicht wissen, wo sie residente Zeichenfolgen platzieren sollen, oder dass die entsprechenden Stellen durch das Framework verborgen sind. Sie brauchen einen gesunden Menschenverstand, wann Es geht um das Kopieren von Zeichenfolgen (z. B. Ländernamen). Die Deduplizierung von Zeichenfolgen eignet sich auch für das Kopieren von Zeichenfolgen zwischen Anwendungen in derselben JVM sowie für Dinge wie XML-Schemas, URLs und JAR-Namen. Es wird allgemein angenommen, dass Zeichenfolgen nicht mehrfach angezeigt werden
Wenn String-Residency im Anwendungsthread auftritt und die Garbage Collection asynchron und gleichzeitig verarbeitet wird, erhöht die String-Deduplizierung auch nicht den Grund, warum wir Thread.sleep() im obigen Code gefunden haben Ohne Ruhezustand wird der GC zu stark beansprucht, sodass die String-Deduplizierung überhaupt nicht erfolgt. Das Problem tritt jedoch nur im Code auf, wenn er ausgeführt wird Deduplizierung.