Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Effective Java

Effective Java
Überblick über Java 9
Teil 2: JVM-Internals
 

Java Magazin, Januar 2017
Klaus Kreft & Angelika Langer

Dies ist die Überarbeitung eines Manuskripts für einen Artikel, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

JVM-Internals

Optimierungen 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 Collection

G1 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ützung

Unified 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.
 
 

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminars:
Seminars
Java Module System - Names, Unnamed, Automatic Modules, Module Descriptors, Tool Chain
1 day seminar ( open enrollment and on-site)
Java 8 & 9 - Lambdas & Stream, New Concurrency Utilities, Date/Time API, Module System
4 day seminar ( open enrollment and on-site)
 

 

  © 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