Unter der Haube

Die wichtigsten Neuerungen aus Java 17 bei Performance, Diagnostik & Tools

Benjamin Schmid @bentolor <ben@tolor.de>

Mehr Schwuppidizität

Mehr Durchblick

Mehr Hilfestellung

Agenda

  1. Garbage Collectoren

  2. Class-Data Sharing

  3. Java Flight Recorder

  4. Foreign Function & Memory

  5. Tooling

  6. Details & Maintenance

Garbage Collectoren

Timeline

11

ZGC Experimental

12

Shenandoah Experimental
G1 Uncommit Memory
ZGC Class Unloading

13

ZGC Uncommit Memory

14

& ZGC Support
CMS Removal
ParalellOldGC Deprecation

15

Shenandoah Production-ready
ZGC Production-ready

16

ZGC Improvements

Die neue Generation der Low-Latency GCs

  • Moderne Architekturen: Multi-Core & TB RAM

  • kurze GC Pausen im ms-Bereich
    → erkauft Responsiveness gegen Durchsatz

  • (fast) vollständig parallel & nebenläufig
    Pausen unabhängig von Größe des Heaps

  • Unterstützen Class Unloading & Uncommit Memory

  • Einfach & Konfigurationsarm

Z Garbage Collector (ZGC)

in place relocation2

-XX:+UseZGC

„A scalable low-latency garbage collector“

Ziele
  • GC Pausen kleiner 10ms 1ms

  • Durchsatz max. -15% gegenüber G1

  • Heapgrößen 8MB – 16TB

  • Einfaches Tuning

Colored Pointers & Load Barriers
→ Object Relocation

Shennadoah GC

„A low-pause-time garbage collector by concurrent evacuation work“

  • ZGC sehr ähnlich Brooks (Forward) Pointers

  • Bietet verschieden Modi & Heuristic-Profile: adaptive, static, compact, aggressive

  • Beil zahlreichen Weak References → ZGC

  • Red Hat Kind → andere Service Offerings

  • Backports für JDK 8 & 11; auch 32-bit

  • ggü. ZGC: abhängig von Root- & Live-Set

-XX:+UseShenandoahGC

shenandoah gc cycle

GC in der Übersicht

GCOptimiert für…Kommentar

G1

Balance

Üblicher Default. Überwiegend Nebenläufig. Zielt auf Balance von Durchsatz & Latenz. Außreißer-Pausen bis 250~800ms. Guter Durchsatz. Häppchenweise Pausen an Zeitbudget orientiert.

Shenandoah

Latenz

Auch verfügbar für JDK8, JDK11 und 32-bit.

ZGC

Latenz

besser für WeakRef; Pausen auch unabhängig Live- und Root-Set

ParallelGC

Durchsatz

Parallel & mehrere Threads. Hoher Durchsatz.
Typische Pausen ~300ms abhängig von Heap-Größe.

SerialGC

Speicherbedarf

Single-Threaded. Empfiehlt sich nur für Heaps ~100MB.

Zing/Azul

Pauseless

Nicht im OpenJDK; nur kommerziell verfügbar

Überblick Änderungen GC’s

ZGC
  • Concurrent Class Unloading 12

  • Uncommit Unused Memory JEP 351 13

  • -XXSoftMaxHeapSize Flag 13

  • Max. Heap Size Increased to 16TB 13

  • ZGC on macOS JEP 364 14

  • ZGC on Windows JEP 365 14

  • ZGC Production-Ready JEP 377 15

  • Concurrent Stack Processing JEP 376 16

Epsilon
  • Epsilon Bug TLABs extension 14

  • Epsilon warns about Xms/Xmx/… 14

G1
  • OldGen on NV-DIMM 12

  • Uncommit Memory 12

  • Improved Sparse PRT Ergonomics 13

  • NUMA-Aware Memory Alloc. JEP 354 14

  • Improved Heap Region Ergonomics 15

  • Concurrently Uncommit Memory 16

Shenandoah
  • Shenandoah (Experimental) JEP 189 12

  • Self-fixing barriers 14

  • Async. object/region pinning 14

  • Concurrent class unloading 14

  • Arraycopy improvements 14

  • Shenandoah Production-Ready JEP 379 15

Bugfixes
  • Disable large pages on Windows 15

  • Disable NUMA Interleaving on Win.15

Legacy
  • ParallelGC Improvements 14

  • Obsolete -XXUseAdaptiveGCBoundary 15

  • Enable Parallel Ref. Processing 17

  • SerialGC Improved young report 13

  • ParalellOldGC: Deprecate JEP 366 14

  • CMS: Remove CMS GC JEP 363 14

Many, many, more…

TL;DR Tipps für den GC

Upgrade lohnt sich!

Probieren geht über Studieren!

Mut zum (probeweisen) Wegwerfen:
Alte Tuning-Parameter

Latenz wichtig? → ZGC oder Shenandoah

Class Data Sharing

Class Data-Sharing in a Nutshell

Class Data-Sharing

Reduziert Startzeiten & Speicherbedarf neuer JVMs durch .jsa Archiv mit Metadaten der Klassen.

→ Klassen liegen vorgeparsed direkt für die JVM verwendbar vor. Das Archiv kann read-only eingebunden werden, was dem OS Caching & Sharing erlaubt.

Achtung: Archive sind JVM Plattform- und Versionspezifisch!

Application Class-Data Sharing (AppCDS)

Erlaubt zusätzlich Applikations-Klassen in das CDS aufzunehmen.

Neuerungen im Bereich CDS

Default CDS Archive 12 JEP 341

JVM liefert nun per Default ein classes.jsa CDS-Archiv mit aus, welches ein Subset der häufigsten JDK-Klassen umfasst.

Dynamic CDS Archive 13 JEP 350

Vereinfacht erheblich die Erstellung eigener AppCDS Archive durch automatische Auswahl und Archiverzeugung beim beenden der Java-Applikation.

AppCDS Archiverstellung

Bisher: Erstellung über Liste 11
$ java -Xshare:off  -XX:DumpLoadedClassList=myclasses.txt -cp myapp.jar MyApp

$ java -Xshare:dump -XX:SharedArchiveFile=myapp.jsa \
       -XX:SharedClassListFile=myclasses.txt -cp myapp.jar
NEU: Automatische Erstellung 13
$ java -XX:ArchiveClassesAtExit=myapp.jsa -cp myapp.jar MyApp
Nutzung des AppCDS-Archives
$ java -XX:SharedArchiveFile=myapp.jsa -cp myapp.jar MyApp
AppCDS Startup Times

JDK Flight Recorder (JFR)

JDK Flight Recorder (JFR) JEP 328

  • OS, JVM, JDK & App Diagnostik

  • extrem geringer Overhead (~1%)

  • built-in & jederzeit aktivierbar

  • always-on möglich → Timemachine

Production Profiling & Monitoring

Flight Recorder Demo

Prozess identifizieren
jcmd
Recording
jcmd <pid> JFR.start
jcmd <pid> JFR.dump \
  filename=record.jfr

Optionen: filename, delay, dumponexit, duration, maxage, maxsize, …

Analysieren
jfr print record.jfr
jfr print \
   --events CPULoad \
   --json record.jfr
jfr summary record.jfr

JFR Event Streaming JEP 349 14 16

JFR Event Streaming API: Beispiel

Reported sekündlich CPU Usage und aktive Locks länger als 10ms:

try (var rs = new RecordingStream()) {
  rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
  rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));

  rs.onEvent("jdk.CPULoad", event -> {
    System.out.println(event.getFloat("machineTotal"));
  });
  rs.onEvent("jdk.JavaMonitorEnter", event -> {
    System.out.println(event.getClass("monitorClass"));
  });

  rs.start(); // Blockierender Aufruf, bis Stream endet/geschlossen wird
  // rs.startAsync(); Alternative im separaten Thread
}

Zugriffsmöglichkeiten

Passiv, eigener Prozess
EventStream.openRepository()) {…}
Passiv, fremder Prozess
EventStream.openRepository(Path.of("…")))
Aktiv, eigener Prozess
try (var stream = new RecordingStream()) { … }
Aktiv, fremder Prozess (Remote)
String url = "service:jmx:rmi:///jndi/rmi://myhost.de:7091/jmxrmi";
JMXConnector c = JMXConnectorFactory.connect(new JMXServiceURL(url));
MBeanServerConnection conn = c.getMBeanServerConnection();

try (RemoteRecordingStream stream = new RemoteRecordingStream(conn)) { … }

Eigene JFR Events

Event definieren
import jdk.jfr.*;

@Name("de.bentolor.ButtonPressed")
@Label("Button Pressed")
@StackTrace(false)
public class ButtonEvent extends Event {
    @Label("Button name")
    public String name;

    @Label("Source")
    public String trigger;

    @Label("Number of Bounces")
    @DataAmount
    public int bounces;

    @Label("Has timeouted")
    public boolean timeouted;
}
Event füttern & auslösen
ButtonEvent evt = new ButtonEvent();
if(evt.isEnabled()) {
    evt.name = "Button 1";
    evt.trigger = "Keyboard";
    evt.begin();
}

// doSomething()

if(evt.isEnabled()) {
    evt.end();
    evt.timeouted = false;
    evt.bounces = 3;
    evt.commit();
}

Weitere Anwendungsfälle

Unit- & Performance-Testing

Annahmen zum Verhalten von API, JVM & Co. in Testcases sichern.

Unterstützende Frameworks z.B. JfrUnit oder QuickPerf

Timeshift-Analyse

Recording mitlaufen lassen und bei Performance-Problemen rückwirkend seit Problemstartpunkt aus dem JFR Event Repository extrahieren & analysieren („Timeshift“)

Foreign Function & Memory API Incubator

Exkurs: Preview features Preview JEP 12

Auslieferung experimenteller Sprach- und JVM-Features,
oft in Iterationen, zur Förderung von frühem Community Feedback.
z.B.: Pattern Matching, Switch Expression, Text Blocks, Records, Sealed Classes

Unlock Compilation
javac --enable-preview …
Unlock Execution
java --enable-preview …


Keine Cross-compilation mittels --release xx möglich!

Exkurs: Incubator Modules Incubator JEP 11

Analog Preview Features für nicht-finale APIs und Tools

javac --add-modules jdk.incubator.foo …
java  --add-modules jdk.incubator.foo …

z.B.: HTTP/2 Client, Packaging Tool, …

Retro: Java Native Interface (JNI)

Java Native Interface Process
  • 26 Jahre alt

  • erfordert .c & .h-Files

  • mehrstufiger Prozess:
    kleinteilig & brüchig

sehr verworren

Motivation Project Panama Incubator JEP 412

Starke Drittbibliotheken (z.B. ML/AI) mit dynamischer Entwicklung
Tensorflow, OpenSSL, libodium, …

Introduce an API by which Java programs can interoperate with code and data outside of the Java runtime […] without the brittleness and danger of JNI.

Ziele: Einfachheit – Performance – Sicherheit

Einfacher Funktionsaufruf

import java.lang.invoke.*;
import jdk.incubator.foreign.*;

class CallPid {
  public static void main(String... p) throws Throwable {
    var libSymbol = CLinker.systemLookup().lookup("getpid").get();          (1)
    var javaSig = MethodType.methodType(long.class);                        (2)
    var nativeSig = FunctionDescriptor.of(CLinker.C_LONG);                  (3)

    CLinker cABI = CLinker.getInstance();
    var getpid = cABI.downcallHandle(libSymbol, javaSig, nativeSig);        (4)

    System.out.println((long) getpid.invokeExact());                        (5)
  }
}
1adressiertes Symbol – hier via Lookup in den System Libraries
2gewünschte Java-Signatur des Java Foreign Handles
3Ziel-Signatur der aufzurufenden C-Funktion
4Funktionshandle beziehen

Aufruf mit Pointer (1/2)

int crypto_box_seal(unsigned char *c, const unsigned char *m,
                    unsigned long long mlen, const unsigned char *pk)

…liest Text aus *m, Zielschlüssel *pk und schreibt verschlüsseltes Ergebnis in nativen Speicher *c

var cryptoBoxSeal = CLinker.getInstance().downcallHandle(
        SymbolLookup.loaderLookup().lookup("crypto_box_seal").get(),
        MethodType.methodType(int.class,
                              MemoryAddress.class, MemoryAddress.class,
                              long.class, MemoryAddress.class),
        FunctionDescriptor.of(C_INT,
                              C_POINTER,   C_POINTER,
                              C_LONG_LONG, C_POINTER) );

Aufruf mit Pointer (2/2)

Foreign Heap wird vom GC via ResourceScope verwaltet
try (var scope = ResourceScope.newConfinedScope()) { … }
String-Konvertierung & Kopie in nativen Heap
var plainMsg = CLinker.toCString("abc", scope);
Reservierung Ziel-Speicherbereich
var cipherText = scope.allocate(48 + plainMsg.byteSize(), scope);
var pubKey = scope.allocateArray(C_CHAR, publicKey);
Aufruf & Rückgabe
var ret = (int) cryptoBoxSeal.invokeExact( cipherText.address(), plainMsg.address(),
                                           (long) plainMsg.byteSize(), pubKey.address());
return cipherText.toByteArray();

Helferlein jextract

Generiert aus direkt aus .h-Dateien passende API Wrapper
als .class oder .java mit den notwendigen Foreign API-Aufrufen.
Nicht direkt in JDK 17 enthalten, sondern via Panama EAP JDK Builds (siehe Link).

$ jextract -t de.bentolor /usr/include/unistd.h
import de.bentolor.unistd_h;

class CallPid {
   public static void main(String[] args) {
      System.out.println( unistd_h.getpid() );
  }
}

jextract Demo (Transcript)

mkdir hello-python
cd hello-python

locate Python.h

jextract -t de.bentolor \
         -l python3.8 \
         -I /usr/include/python3.8/ \
         -I /usr/include/ \
         /usr/include/python3.8/Python.h

joe Schlange.java

java --add-modules jdk.incubator.foreign \
     --enable-native-access=ALL-UNNAMED \
     -Djava.library.path=/usr/lib/x86_64-linux-gnu/ \
     Schlange.java

jextract -t de.bentolor \
         -l python3.8 \
         -I /usr/include/python3.8/ \
         -I /usr/include/ \
         --source
         /usr/include/python3.8/Python.h

bat de/bentolor/Python_h.java

bat de/bentolor/Python_h_4.java
/s int PyRun_S
import jdk.incubator.foreign.*;
import de.bentolor.Python_h;

public class Schlange {
  public static void main(String[] args) {
    String script = """
            print(sum([33, 55, 66]));
            print('Hello Python 3!')
            """;

    Python_h.Py_Initialize();
    try (var scope = ResourceScope.newConfinedScope()) {
        var str = CLinker.toCString(script, scope);
        Python_h.PyRun_SimpleStringFlags(
              str, MemoryAddress.NULL);
        Python_h.Py_Finalize();
    }
  }
}

Tooling

jpackage JEP 343 JEP 392

Werkzeug zum Erstellen & Paketieren eigenständiger Java-Applikationen

Native Installer

.msi und .exe
.pkg und .dmg
.deb und .rpm

Konfiguration

Start-Optionen (JVM/App)
Meta-Daten
Datei-Assoziationen

Nicht im Scope

Splash-Screen
Auto-Update Mechanismus

javadoc

Das Javadoc-Tool hat mit JDK16 umfassende Verbesserungen erfahren…

  • Verbesserte Suche

  • Fehler zeigen Code-Ausschnitt

  • Neues/Verbessertes New, Deprecated, Related Package

  • Mobile-friendly Layout

  • autom. Links zur JDK API

  • Checks für leere Absätze

  • Bessere "Typ"-Terminologie

  • Bessere Darstellung von @see, Paketen, Nested Class, u.a.

{@return …}-Shortcut
/** {@return The max value in the array} */
public static int max(final int... array) {


Javadoc Result

Details & Maintenance

Hilfreiche Nullpointers JEP 358 14

class MyClass {
    record Person(String name, String email) {}
    public static void main(String[] args) {
        var p = new Person("Peter", null);                   (1)
        var e = p.email().toLowerCase();
    }
}
$ java MyClass.java
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase() because the return value of "MyClass$Person.email()" is null
        at MyClass.main(MyClass.java:5)
1Für Namen von lokalen Variablen und Lambdas mit -g:vars compilieren!

„Jahresinspektion“

  • Strongly Encapsulate JDK Internals JEP 391

  • macOS/AArch64 Port JEP 391

  • SecurityManager forRemoval JEP 411

  • Always-Strict Floating-Point Semantics JEP 306

  • Asynchrones Unified JVM Logging (-Xlog:async)

  • Ausführlichere Crashs: -XX:+ExtensiveErrorReports

  • Unicode 10 → 13; CLDR 33 → 39

  • Krypto: Deprecated Ciphers/Signatures, Enhanced PRNG JEP 356

GraalVM

GraalVM — Polyglot VM

  • Ahead-of Time compiler (AoT)

  • Polyglotte VM für div. Sprachen
    JVM (Java, Kotlin, Scala, …)
    LLVM (C, C++) → native
    Java Script, Python, Ruby, R

  • Sprachen sharen Runtime
    → Zero Interop Overhead

  • Native executables (SubstrateVM)
    → Kleiner Startup & Memory

  • GraalVM Community & Enterprise

Microservice Frameworks

Helidon, Quarkus.io, Micronaut, Spring Fu, Ktor, …
→ zielen auf GraalVM AoT & Microservices, z.B. via IoC zur Compiletime

Vielen Dank!