|
|||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||
|
Effective Java
|
||||||||||||||||||
JVM-InternalsOptimierungen im Zusammenhang mit Strings
In vielen Java-Applikation werden große
Mengen an
String
s angelegt und verarbeitet.
Man hat beispielsweise festgestellt, dass eine "typische Java-Applikation"
ungefähr 25% des Speichers mit
String
s
belegt. Deshalb sind Optimierungen im Zusammenhang mit
String
s
eingebaut worden. Ein Ziel war es, den Speicherbedarf von
String
s
zu reduzieren. Die Konkatenierung von
String
s
wurde überarbeitet und das Handling von "interned"
String
s
wurde verbessert.
String-Deduplikation
Mit der Reduktion des Speicherbedarfs von
String
s
wurde bereits in Java 8 begonnen. Mit JDK 8_u20 wurde die sogenannte
String
-Deduplikation
in den G1 (Garbage First) Garbage Collector eingebaut (siehe /
SDUP
/).
Dieser Garbage Collection Algorithmus macht keine
String
-Deduplikation
im eigentlichen Sinne, sondern er sucht nach
char
-Arrays
gleichen Inhalts. Das kann ein Garbage Collector bequem machen, weil
er ohnehin im Rahmen der Garbage Collection die erreichbaren Objekte besuchen
muss. Er baut dabei einen Cache mit
char
-Arrays
auf, die zu
String
-Objekten gehören.
Wenn er Duplikate unter den
char
-Arrays
findet und sie ein gewisses Alter erreicht haben (siehe
-XX:StringDeduplicationAgeThreshold
)
,
dann sorgt er dafür, dass die zugehörigen
String
-Objekte
gemeinsam das eine
char
-Array im Cache
benutzen. Durch die beschriebene Deduplikation wird die Zahl der
char
-Arrays
und damit der Speicherbedarf der
String
s
insgesamt reduziert. Eine solche Optimierung kann man für
String
s
machen, weil
String
ein unveränderlicher
Typ ist und
String
s ihre
char
-Arrays
nicht modifizieren. Für
StringBuilder
,
StringBuffer
oder andere veränderliche Zeichenketten geht es natürlich nicht.
Die
String
-Deplikation
funktioniert nur mit dem G1-Garbage-Collector. Man muss dafür folgende
JVM-Optionen setzen:
-XX:+UseG1GC -XX:+UseStringDeduplication
Der G1-Garbage-Collector ist in Java 9 bereits per Default gesetzt, so dass man in Java 9 nur noch die Deduplikation explizit einschalten muss. String-Kompaktierung
Das Ziel, den Speicherverbrauch von
String
s
zu reduzieren, wird u.a. mit der
String
-Kompaktierung
verfolgt (siehe /
SCMP
/).
Man hat festgestellt, dass die allermeisten
String
s
nur ISO-8859-1/Latin-1-Zeichen enthalten. Das sind Kodierungen, für
die man nur ein Byte pro Zeichen braucht. In Java enthalten
String
s
aber
char
-Arrays und jeder Character
ist in Java zwei Byte groß. Da man für allerwenigsten Zeichen wirklich
zwei Bytes für die Darstellung braucht, verschwenden
String
s
meistens doppelt so viel Speicher, wie nötig wäre.
Mit der
String
-Kompaktierung
wird die Implementierung von Klassen wie
String
,
StringBuilder
,
StringBuffer
,
etc. geändert. Anstelle eines
char
-Arrays
enthalten sie in Java 9 ein
byte
-Array
zzgl. eines Encoding-Flags. Bei der Konstruktion wird nachgeschaut, ob
alle Zeichen in einem Byte dargestellt werden können oder ob es Zeichen
gibt, für die zwei Bytes benötigt werden. Das Ergebnis dieser Prüfung
wird im Encoding-Flag vermerkt. Wenn sich alle Zeichen mit einem Byte
darstellen lassen, dann werden sie platzsparend in je einem Byte dargestellt.
In den wenigen Fällen, in denen einige Zeichen tatsächlich zwei Byte
für die Darstellung benötigen, wird das Flag entsprechend gesetzt und
das
byte
-Array wird als
char
-Array
interpretiert.
Die String -Kompaktierung ist per Default aktiviert. Man kann sie mit -XX:-CompactStrings abschalten. Interned Strings in CDS-(Class Data S h aring)-Archiven
CDS (Class Data Sharing) gibt es seit Java
5. Es wurde entwickelt, um die Startup-Zeiten und den Speicherverbrauch
von Java-Applikationen zu reduzieren, die auf derselben Maschine laufen
und dieselbe JDK-Installation verwenden. Die Idee ist, dass der JRE-Installer
solche Klassen aus dem
rt.jar
, die in
jeder Java-Applikation verwendet werden, in ein sogenanntes "shared archive"
legt. Dieses Archiv verwenden alle JVMs auf einer Maschine gemeinsam.
Wenn eine JVM startet, dann kann sie die Klassen aus dem Archiv einfach
in den Speicher laden, ohne ein echtes Class Loading machen zu müssen.
In Java 9 werden nun auch die "Interned Strings"
und die
String
s aus dem "Constant Pool"
der Klassen mit in das Shared Archive gelegt und beim Startup einer JVM
einfach in den Speicher geladen, ohne dass die
String
-Objekte
per Allokation und Konstruktion einzeln erzeugt werden müssen (siehe /
SCDS
/).
Im Constant Pool einer Klasse befinden sich u. a.
String
-Literale,
die bei der Übersetzung im Code der Klasse gefunden wurden. Interned
Strings sind solche
String
s, die explizit
mit der
intern()
-Methode der Klasse
String
in einen
String
-Pool gelegt wurden, der
Duplikate zurückweist. Das
String
-Interning
ist eine Art manuelle
String
-Deduplikation,
die man unter Verwendung der
intern()
-Methode
selber programmieren muss. Weitere Informationen findet man zum Beispiel
unter /
INTN
/.
Das Ablegen der oben genannten String s in CDS-Archiven wird nur unterstützt für 64-Bit-Systeme und nur wenn der Garbage First (G1) Garbage Collector verwendet wird. Das liegt daran, dass der Zugriff auf die geladene String -Tabelle optimiert ist und nur funktioniert, wenn die String -Objekte nicht verschoben werden. Dafür braucht man einen Garbage Collector, der sogenannte "pinned regions" unterstützt, d.h. Speicherbereiche, in denen die Objekte nicht verschoben werden. Der G1 Collector ist der einzige Garbage Collector in der Hotspot-JVM, der solche "pinned regions" verwaltet. Indify String Concatenation
Bei der "Indification" der
String
-Konkatenierung
(siehe /
SIND
/)
geht es um Interna der JVM, von denen wir als Java-Entwickler kaum etwas
bemerken sollten.
Bekanntermaßen ist die Konkatenierung von
String
s
relativ teuer, weil
String
s unveränderlich
sind und für die Konkatenierung ein neues
String
-Objekt
erzeugt werden muss, in das alle Zeichen umkopiert werden. Das ist besonders
aufwändig, wenn ganze Ketten von
String
-
+
-
oder -
concat()
-Operationen abgearbeitet werden
müssen.
Um diesen Kopieraufwand und das Erzeugen
der vielen temporären
String
s zu reduzieren,
ersetzt seit Java 5 der
javac
-Compiler
die Kette von
String
-Konkatenierungen
durch eine Kette von
append()
-Aufrufen
der Klassen
StringBuilder
oder
StringBuffer
.
Gleichzeitig kann man mit
-XX:+OptimizeStringConcat
diverse Optimierungen an den
append()
-Ketten
einschalten, die dann zur Laufzeit vom JIT-Compiler gemacht werden. Es
gibt also zwei Baustellen, an denen die
String
-Konkatenierung
optimiert wird: den
javac
-Compiler und
den JIT-Compiler.
Um zukünftige Optimierungen einfacher implementieren zu können, hat man die String -Konkatenierung in Java 9 so umgebaut, dass der javac -Compiler für eine String -Konkatenierung nur noch einen invokedynamic -Bytecode (auch als INDY abgekürzt) generiert. Er optimiert also selber nichts mehr und überlässt alles dem JIT-Compiler. Den INDY-ByteCcode gibt es seit Java 7 und er wird seit Java 8 u. a. für die Übersetzung von Lambda-Ausdrücken verwendet. invokedynamic unterscheidet sich von den anderen invoke -Bytecodes ( invokevirtual , invokestatic und invokespecial ) dadurch, dass nicht festgelegt ist, wie der Methodenaufruf erfolgen soll. Stattdessen wird beim invokedynamic eine Meta-Factory mitgegeben, die im Wesentlichen die Beschreibung liefert, was invokedynamic eigentlich machen soll. Die betreffenden Factories findet man im Package java.lang.invoke ; dort gibt es seit Java 8 eine LambdaMetafactory und nun auch eine StringConcatFactory . Garbage CollectionG1 ist der Default-Garbage-Collector
An der Garbage Collection selbst ändert
sich mit Java 9 nicht viel. Das Interessanteste ist sicher, dass der Garbage
First (G1) Collector in Java 9 nun der Default-Algorithmus ist (siehe /
GCDF
/).
Er wurde mit JDK 6_u14 erstmals als experimentelles Feature zur Verfügung
gestellt und war damals als Alternative zum Concurrent-Mark-and-Sweep (CMS)-Algorithmus
gedacht. Das Hauptziel dieser beide Garbage Collectoren ist die Reduktion
der Stop-the-World-Pausen, in denen die Threads der Applikation angehalten
werden. Anders als CMS und alle anderen traditionellen Garbage Collectoren
organisiert der G1-Collector den Heap nicht in zusammenhängende Bereiche
für junge und alte Objekte (die sogenannten Generationen), sondern er
verwaltet viele, kleine Regionen gleicher Größe, die je nach Bedarf für
die Allokation junger oder die Evakuierung alter Objekte verwendet werden.
Eine ausführliche Beschreibung des G1-Collectors und seinen Tuning-Möglichkeiten
findet man in dem Buch " Java Performance Companion" von Charlie Hunt,
Monica Beckwith et.al. (siehe /
G1GC
/).
Mittlerweile ist der G1-Collector so ausgereift, dass er für viele Applikation
bessere Ergebnisse bei den Pausenzeiten und auch beim Durchsatz erzielt
als die älteren Garbage Collectoren. Deshalb ist er ab Java 9 der Default-Garbage-Collector.
Weitere Informationen zum G1 findet man in unseren Artikeln im Java Magazin
aus dem Februar und April 2011 (siehe /
G1A1
/
und /
G1A2
/)
und in dem Buch "Java Performance Companion" (siehe /
G1BK
/).
Einige GC-Flags-Kombinationen verschwinden
Gewisse Kombinationen von Flags für die
Garbage Collectoren sind seit Java 8 "deprecated" und werden in Java 9
nicht mehr unterstützt. Die Liste der Kombinationen findet man unter
/
GCFL
/.
Die meisten davon betreffen den CMS-Collector.
Erhebliche Änderungen beim GC-Trace-Output
Der Trace-Output der Garbage Collectoren
ändert sich radikal in Java 9. Das hängt damit zusammen, dass an sich
das Logging der JVM vereinheitlicht wurde (siehe /
UNLG
/).
Hier ein Beispiel für den veränderten Output. Was in Java 8 mit
der Option
-XX:+PrintGCDetails
so aussah:
[GC (Allocation Failure) [PSYoungGen: 31719K->5090K(36864K)] 31719K->28421K(121856K), 0.0294801 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] [GC (Allocation Failure) [PSYoungGen: 36815K->5090K(68608K)] 60146K->60053K(153600K), 0.0308231 secs] [Times: user=0.02 sys=0.03, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 5090K->0K(68608K)]
[ParOldGen: 54962K->60024K(132096K)] 60053K->60024K(200704K), [Metaspace:
4029K->4029K(1056768K)], 0.0356721 secs] [Times: user=0.02 sys=0.00, real=0.03
secs]
sieht jetzt in Java 9 mit der Option
-Xlog:gc=trace
so aus:
[0.363s][trace][gc] GC(0) PSYoung generation size changed: 41984K->73728K [0.363s][info ][gc] GC(0) Pause Young (Allocation Failure) 30M->26M(119M) 25.453ms [0.381s][info ][gc] GC(1) Pause Young (Allocation Failure) 57M->57M(150M) 14.919ms [0.394s][debug][gc] GC(2) Expanding ParOldGen from 84992K by 42496K to 127488K [0.394s][trace][gc] GC(2) PSYoung generation size changed: 73728K->133120K
[0.394s][info ][gc] GC(2) Pause Full (Ergonomics)
57M->57M(191M) 13.279ms
Diagnose-UnterstützungUnified JVM-Logging
Die Logging-Ausgaben der Hotspot-JVM waren
bislang uneinheitlich. Jede JVM-Komponente (Garbage Collector, JIT-Compiler,
usw.) hatte ihre eigene Art und Weise, was sie wann in welchem Format und
in welcher Ausführlichkeit ausgibt und mit welchen JVM-Flags es gesteuert
wird. Mit Java 9 sind die Logging-Ausgaben vereinheitlicht worden (siehe
/
UNLG
/).
Oracle hat neben der HotSpot-JVM noch eine zweite JVM, die JRockit-JVM.
Die beiden JVMs sollen langfristig zusammengeführt werden indem Features
aus der JRockit-JVM in die HotSpot-JVM eingebaut werden. Die Vereinheitlichung
der Logging-Ausgaben ist ein solches Feature, das durch die JRockit-JVM
inspiriert ist, die im Vergleich zur Hotspot-JVM schon immer deutlich bessere
Unterstützung für die Diagnose von JVM-Problemen hatte.
Die Überarbeitung des JVM-Loggings in der
Hotspot-JVM von Java 9 geht damit los, dass es nur noch ein einziges JVM-Flag
-Xlog
für die Steuerung sämtlicher Logging-Ausgaben der JVM gibt. Man kann
der
-Xlog
-Option verschiedene Parameter
mitgeben. Beispielsweise sind die Logging-Ausgaben in Kategorien eingeteilt
(z.B. gc, compiler, classload, …). Es gibt verschiedene Log-Levels
(error, warning, info, debug, trace). Es gibt Decorators (z.B Zeitstempel).
Die Log-File-Rotation, die es für die Garbage Collections Logs schon länger
gibt, wird jetzt für alle JVM-Logs unterstützt. Außerdem lässt sich
das Logging nicht nur über die
-Xlog
-JVM-Option,
sondern auch noch zur Laufzeit über das
jcmd
-Tool
steuern (
mit jcmd <pid> VM.log
[options]
).
Insgesamt ist die Steuerung einheitlicher,
aber insgesamt immer noch sehr komplex, da man eine Vielzahl von Parametern
mit und ohne Wildcards kombinieren kann. Hinzu kommt, dass viele alte
JVM-Flags, die den Trace-Output beeinflusst haben, in Java 9 nicht mehr
unterstützt werden (z.B.
-XX:+PrintAdaptiveSizePolicy
,
-XX:+PrintHeapAtGC
,
…). Man kann ähnliche Effekte über die Parameter der neuen
-Xlog
-Option
erzielen, aber 1:1 ist der Ersatz der alten
-XX:+Print
-Flags
durch Parameter der neuen
-Xlog
-Option
nicht. Das könnte erheblichen Umstellungsaufwand bei der Diagnose von
JVM-Probleme in Java 9 bedeuten.
Hier ein Beispiel für Gegenüberstellung
von alten und neuen Logging-Ausgaben.
Wir verwenden zur Illustration den G1-Collector
und steuern die Logging-Ausgabe über die alten Flags
-XX:+PrintGCTimeStamps
und
-XX:+PrintAdaptiveSizePolicy
. Die
Ausgabe sieht in Java 8 so aus:
0.342: [G1Ergonomics ( CSet Construction ) start choosing CSet, _pending_cards: 527, predicted base time: 6.88 ms, remaining time: 193.12 ms, target pause time: 200.00 ms] 0.342: [G1Ergonomics ( CSet Construction ) add young regions to CSet, eden: 41 regions, survivors: 5 regions, predicted young region time: 191.48 ms] 0.342: [G1Ergonomics ( CSet Construction ) finish choosing CSet, eden: 41 regions, survivors: 5 regions, old: 0 regions, predicted pause time: 198.36 ms, target pause time: 200.00 ms] 0.344: [G1Ergonomics ( Heap Sizing ) attempt heap expansion, reason: recent GC overhead higher than threshold after GC, recent GC overhead: 17.68 %, threshold: 10.00 %, uncommitted: 1547698176 bytes, calculated expansion amount: 309539635 bytes (20.00 %)]
0.344: [G1Ergonomics (
Heap
Sizing
) expand the heap, requested expansion amount: 309539635 bytes,
attempted expansion amount: 310378496 bytes]
Man sieht den Zeitstempel und die Informationen
der AdaptiveSizePolicy wie etwa die Überlegungen zum Aufbau des Collection
Sets und zur Anpassung der Heap-Größe.
Und hier in etwa die gleiche Menge an Information
in Java 9 mit dem neuen
-Xlog
-Flag, Genauer
gesagt haben wir
-
Xlog:gc+ergo*=trace::uptime,tags
:
spezifiziert. Die Ausgabe sieht dann in Java 9 so aus:
[0.382s][gc,ergo, cset ] GC(4) Start choosing CSet. pending cards: 1028 predicted base time: 9.50ms remaining time: 190.50ms target pause time: 200.00ms [0.382s][gc,ergo, cset ] GC(4) Add young regions to CSet. eden: 16 regions, survivors: 3 regions, predicted young region time: 126.39ms, target pause time: 200.00ms [0.382s][gc,ergo, cset ] GC(4) Finish choosing CSet. old: 0 regions, predicted old region time: 0.00ms, time remaining: 64.11 [0.386s][gc,ergo ] GC(4) Running G1 Clear Card Table Task using 1 workers for 1 units of work for 19 regions. [0.386s][gc,ergo ] GC(4) Running G1 Free Collection Set using 1 workers for collection set length 19 [0.386s][gc,ergo, heap ] GC(4) Attempt heap expansion (recent GC overhead higher than threshold after GC) recent GC overhead: 4.25 % threshold: 1.00 % uncommitted: 1937768448B base expansion amount and scale: 130023424B (38.97%) [0.386s][gc,ergo, heap ] GC(4) Expand the heap. requested expansion amount:50671711B expansion amount:51380224B [0.387s][gc,ergo,refine] GC(4) Updating Refinement Zones: update_rs time: 0.029ms, update_rs buffers: 5, update_rs goal time: 19.999ms
[0.387s][gc,ergo,refine] GC(4) Updated Refinement
Zones: green: 6, yellow: 18, red: 30
Man findet die gewohnten Logging-Ausgaben
wieder, aber man sieht auch, dass die Steuerung über die
-Xlog
-Option
nicht ganz trivial und etwas gewöhnungsbedürftig ist. Zur Erläuterung:
wir haben mit
gc+ergo*=trace
ausführliche
Ausgaben zu den Kategorien "gc" und "ergo" in Kombination mit anderen Kategorien
angefordert und wir haben mit den Dekoratoren
uptime,tags
die Zeitstempel und die Ausgabe der Kategorien (wie z.B. [gc,ergo,
cset
])
verlangt.
|
|||||||||||||||||||
© Copyright 1995-2018 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/91.Java9.What-is-new-in-Java-9/90.java-9.1.overview.ready_4.html> last update: 26 Oct 2018 |