© BlurryMe/Shutterstock.com
JEP 387: Elastic Metaspace

Ein neues Klassenzimmer


Die JVM braucht Speicher zum Leben und davon leider manchmal viel. Einer der größten nativen Verbraucher kann der Metaspace sein. Java 16 bringt mit JEP 387: Elastic Metaspace eine Neuimplementierung dieses Subsystems, die es schlanker und sparsamer macht.

Java 16 ist erschienen und bringt eine ganze Reihe interessanter Neuerungen. Es gibt neue Sprachfeatures wie das Vector-API und die nun offiziellen Records, neue Plattform-Ports für Windows AArch64 und Alpine Linux und viele Verbesserungen an der JVM selbst. Zu diesen Verbesserungen gehört JEP 387: Elastic Metaspace [1].

Interessant an diesem JEP ist unter anderem, dass er zu den wenigen gehört, die nicht aus Oracles Entwicklungsabteilung stammen. Vielmehr wurde dieser JEP von SAP entwickelt und gesponsert. Er zählt zu einem der umfangreichsten Patches, die von außen ins OpenJDK eingebracht wurden. Aber was verbirgt sich hinter diesem JEP?

Den Metaspace zähmen

Die JVM kann ausgesprochen speicherhungrig sein. Da der höchste Verbrauch typischerweise beim Java Heap liegt, konzentrieren sich viele JVM-Entwickler auf die Optimierung der Garbage Collectors: Projekte wie Red Hats Shenandoah und Oracles ZGC ziehen viel Aufmerksamkeit auf sich und das vollkommen zu Recht.

Der Java Heap ist aber nur ein Teil der Story. Manche Benutzer sind erstaunt, wenn der Speicherverbrauch ihrer JVM-Prozesse deren eingestellte Heap-Größe deutlich überschreitet. Das ist jedoch nicht verwunderlich, denn die JVM benötigt neben dem Java Heap auch anderen Speicher. Thread-Stacks, Kontrollstrukturen der GC, Class-Data-Sharing-Archive, Textsegmente, Datenstrukturen des Just-in-Time-Compilers und dessen Kompilate – der sogenannte Codecache – sind nur einige Beispiele für JVM-interne Daten, die außerhalb des Java Heaps leben. Die Gesamtheit dieser Speicherbereiche wird oft als „Off-Heap“ oder, etwas unpräziser, als „nativer“ Speicher bezeichnet. Die Summe dieses nativen Speichers kann erheblich sein und die Größe des Java Heaps überschreiten.

Einer der größten Verbraucher nativen Speichers ist häufig der Metaspace, ein Bereich, der Klassenmetadaten hält. Ist er begrenzt und läuft voll, kommt es zum OOM-(Out-of-Memory-)Error. Leider sind dann die Handlungsmöglichkeiten begrenzt: Endanwender der JVM können lediglich die Limits hochsetzen (MaxMetaspaceSize oder CompressedClassSpaceSize). Java-Entwickler können ihre Programme umschreiben, um weniger Klassen zu laden oder Class Loader früher freizugeben. Aber großen Spielraum gibt es nicht, denn das Problem ist einfach, dass der Metaspace bestimmte Allokationsmuster nicht besonders gut verträgt. Genau dort hakt JEP 387 ein. Der neue Metaspace in Java 16 verbraucht weniger Speicher und gibt ihn bereitwilliger wieder her, er ist also sparsamer und elastischer.

Klassenmetadaten ...

Eine Java-Klasse besteht aus sehr viel mehr als nur dem Objekt java.lang.Class. Lädt die JVM eine Klasse, baut sie im Speicher einen Baum aus Verwaltungsstrukturen auf, der die Klasse und ihre Methoden beschreibt. Die Daten in diesem Baum entsprechen größtenteils dem analysiertem und aufbereiteten Laufzeitabbild der Klassendatei [2]. Die Wurzel des Baumes bildet eine (tatsächlich mit „K“ geschriebene) Klass-Struktur. Weiterhin finden sich hier Tabellen für Interfaces [3] und virtuelle Calls [4], der Konstantenpool, die Methodenstrukturen, Annotationen, Bytecode und vieles mehr. Dazu zählen auch Daten, die nicht aus der Klassendatei stammen, sondern zur Laufzeit generiert werden, wie zum Beispiel JIT-spezifische Zähler.

… und ihr Lebenszyklus

Eine Klasse wird über einen ClassLoader geladen. Dieser erzeugt das java.lang.Class-Objekt im Heap und speichert die gelesenen Metadaten. Im Laufe seines Lebens lädt der Loader Klassen und die Größe der angesammelten Metadaten steigt.

Das Entladen der Klassen erfolgt gesammelt erst dann, wenn ihr ClassLoader stirbt. Das schreibt die Java-Spezifikation so vor: „A class or interface may be unloaded if and only if its defining class loader may be reclaimed by the garbage collector“. [5]

Dieser Satz hat interessante Konsequenzen: Jede Klasse hält eine Referenz auf den ClassLoader, der sie geladen hat. Eine Klasse wiederum wird von ihren Instanzen am Leben gehalten. Um also einen ClassLoader abräumen zu können, darf es keine Instanzen seiner Klassen mehr geben, die Klassen selbst müssen unerreichbar sein, und es darf keine Referenz mehr auf den ClassLoader zeigen. Erst dann wird der ClassLoader vom Garbage Collector erfasst und alle seine Klassen werden entladen. In diesem Moment werden auch die angesammelten Metadaten abgeräumt. Wir haben es also mit einem „Bulk-Free“-Szenario zu tun.

Ausnahmen bestätigen die Regel, und so gibt es auch in der JVM den Fall, dass Metadaten vor dem Ableben des Loaders freigegeben werden. Das passiert zum Beispiel bei Klassenredefinition: Der alte Bytecode wird durch neuen Bytecode ersetzt und danach nicht mehr gebraucht, obwohl der ClassLoader ja weiterlebt. Ein weiterer Fall tritt dann auf, wenn eine Klasse nicht vollständig geladen werden kann, aber Teile ihrer Metadaten schon im Speicher liegen. Diese Fälle sind aber Ausnahmen und betreffen nur einen geringen Teil der Metadaten.

Die Permanent Generation

Metadaten leben heute im Metaspace, aber das war nicht immer so. Vor Java 8 lagen sie im Java Heap in einer speziellen Generation, der sogenannten Permanent Generation. Sie wurden wie andere Objekte auch von der Garbage Collection verwaltet. Das verursachte Probleme.

Als Teil des Java Heaps war die Permanent Generation durch diesen begrenzt und ihre maximale Größe musste beim Start der JVM festgelegt werden. Klassen und Class Loader leben in der Regel länger als normale Objekte, daher hatten GCs geringere Chancen, Speicher in der Permanent Generation freizuräumen. Eine zu kleine Permanent Generation war in der Regel fatal. Im Zweifel dimensionierte man deshalb die Permanent Generation unnötig großzügig, und das auf Kosten der anderen Generations.

Ein weiteres Problem der Permanent Generation war der Aufwand, den man betreiben musste, um Metadaten...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang