In-Vivo JVM Heap Introspection & Observability

Senior Software Engineer @ Chrono24

Insight - Der Chrono24 Blog

Vom Integrations- zum Entwicklungsprojekt

Motivation

Angepasste und effiziente Cache-Mechanismen sind unentbehrlich für niedrige Latenz und hohe Leistung, nicht nur beim Betrieb einer Online-Plattform. Es sei denn, die gesamte Datenmenge passt in den Hauptspeicher. Besonders im Fall von Daten mit zufällig verteilten Zugriffsmustern bietet ein 100%-iger In-Memory-Ansatz gleichförmige, vorhersagbare, niedrige Latenz.

Da die Menge der Daten stetig wächst und die Nachteile einer einfachen Vergrößerung des Heap zunehmend kompromissbehaftet und teurer werden, tut man gut daran, die Instanzen möglichst speichereffizent abzubilden. Um dies zu optimieren, benötigt man Einblicke, die über den Code der Anwendung hinausgehen:

  • das Speicherlayout eigener Klassen und ihrer Felder,
  • ihre Anzahl und die Belegungshäufigkeit innerer Referenzen, sowie
  • die Verteilung tatsächlicher Anzahl, Größe und Verwendung von Standard-Klassen wie Collections oder Strings,
  • und all dies im Produktivbetrieb.

Lösung mit Bordmitteln?

Man möchte meinen, dass die Java Virtual Machine (JVM) mit all ihren Werkzeugen diese Art von Informationen bereitstellen kann. Es gibt dabei aber ein paar Haken.

Der kürzeste Weg zu einer Momentaufnahme wäre der Heap Dump. Dieser kann von der Produktivumgebung abgezogen und anderswo analysiert werden, sodass die Auswertung keine kostbaren Ressourcen bindet, auch wenn es ein Problem für sich sein kann, eine Datei dieser Größe zu transportieren.

Ein aussagekräftiger Heap Dump enthält nur lebende Objekte. Um diese Anforderung zu erfüllen, ist eine vollständige Garbage Collection (GC) notwendig. Diese sollte weitestgehend vermieden werden, weil sie trotz aller Parallelisierungsbemühungen der jüngeren Vergangenheit immer noch für einen gewissen Zeitraum die Ausführung der gesamten Anwendung anhält. Abhängig von der Größe und Zusammensetzung des Heap kann diese „stop-the-world GC“ leicht eine Größenordnung von Minuten erreichen. Selbst die Schritte der GC, die — normalerweise ohne schädliche Wirkung — parallel zur Anwendung laufen, werden durch die hohe Intensität der Performance abträglich.

Ein solcher Leistungsabfall ist normalerweise nicht tragbar und man müsste die zu untersuchende Anwendung aus dem Load Balancing entfernen – vorausgesetzt, der Betrieb ist redundant und kann kurzzeitig einer solchen Ressourcenverknappung standhalten.

Abgesehen von den Schwierigkeiten, einen Heap Dump zu beschaffen, bringen die Werkzeuge zur Analyse ihre individuellen Vor- und Nachteile mit sich. VisualVM z.B. benötigt bei einem Dump von rund 60 GiB mehrere Minuten, allein um die Datei zu öffnen; Objekte nach zurückgehaltener Größe zu sortieren, kann Tage dauern, selbst wenn das Arbeitsverzeichnis auf einer SSD der schnelleren Sorte liegt. Der Eclipse Memory Analyzer (MAT) benötigt auch einen Moment, aber nicht annähernd so lange wie VisualVM. Kommerzielle Lösungen auf der anderen Seite sind teils sehr mächtig, aber prohibitiv teuer für den täglichen Einsatz durch viele Entwickler.

Java Mission Control kombiniert mit Flight Recorder bieten nahezu Live-Monitoring einer Fülle von Metriken, bis hin zu Allokationsverhalten von Klassen und Code-Profiling. Je näher wir aber einer Momentaufnahme des Heap-Inhalts kommen, um so involvierter wird auch hier die GC.

Integration einer bestehenden Bibliothek in die Anwendung

Da die Instrumentierung der JVM in der Praxis eher unbefriedigende Ergebnisse liefert, stellt sich die Frage nach programmatischen Lösungen.

Das Projekt Java Object Layout (JOL) bietet seit Jahren tiefe Einblicke in die Interna der JVM, u.a. in das tatsächliche Layout von Klassen im Speicher. Ausgehend von einem oder mehreren Wurzelobjekten kann auch der Inhalt des gesamten dahinter verborgenen Referenzbaums analysiert werden. Die Maven-Abhängigkeit war schnell zum Projekt hinzugefügt. Von dort ging es bis zum ersten GraphLayout eines bekannten Speicherfressers nur noch bergab. Jedenfalls was die codeseitige Integration betraf; die ersten Testergebnisse waren dagegen durchwachsen.

Zum einen enthält ein GraphLayout Referenzen auf alle durchlaufenen Objekte. Bei Klassen, die einem stetigen Austausch unterliegen, ist diese Rückhaltung problematisch.

Weiter ist es nicht gerade klein. 1 GiB Echtdaten ergeben z.B. insgesamt 2.2 GiB, die im GraphLayout enthalten sind. Bedenkt man die Rechenzeit für die Erstellung und dass man keine sekundenaktuellen Ergebnisse braucht, so möchte man sie für eine sinnvolle Zeit zwischenspeichern. Bei dieser Größenordnung ist Caching aber undenkbar.

Schließlich sind die Ausgabeformate entweder nicht detailliert genug, oder aber viel zu geschwätzig: GraphLayout.toFootprint() liefert nur den Gesamtverbrauch jeder Klasse ohne Hinweis über die Verschachtelung, wohingegen GraphLayout.toPrintable() die tatsächlichen Speicherinhalte auf den Punkt widerspiegelt, ohne einen Hauch von Aggregation. Im letzteren Fall ist es nicht weiter schwierig, die Ausgaberoutine in eine Out-Of-Memory-Situation zu bringen.

Während also die Grundfunktionalität sehr zu begrüßen ist, lassen die Möglichkeiten zur Integration noch etwas zu wünschen übrig. Der Proof-Of-Concept (POC) betreffend trivialer Integration musste daher nach dem Fail-Fast-Ansatz als Fehlschlag deklariert werden.

Unveränderte Integration gescheitert, Anpassung möglich

Dank des Open-Source-Charakters des Originalprojekts ist es möglich, bestehende Funktionalität einfach zu ergänzen.

Der Aggregationscharakter der gewünschten Ausgaben legt eine oder mehrere Aggregationen direkt während des Durchlaufens der Speicherinhalte nahe. Somit verhält sich die Größe der Zwischenergebnisse proportional zur Komplexität der Strukturen, nicht zur Anzahl der betrachteten Objekte. Das macht Hoffnung auf verschiedene Ausgabetypen, die alle mit einem einzigen Heap-Durchlauf vorbereitet und gecached werden können, bis auf Anfrage eine lesbare Ausgabe erzeugt wird.

Grundkonzept ist ein Drill-Down in zwei Richtungen:

  • Top-Down von beliebigem Wurzel-Objekt aus den Referenz-Graphen entlang, mit Fokus auf die zurückgehaltene Größe unterhalb.
  • Bottom-Up als Verbrauch pro Klasse, aggregiert nach Verwendung, mit Fokus auf die flache eigene Größe, gruppiert nach Verwendung.
  • Gruppierung jeweils nach enthaltendem Klasse-Feld-Paar.

Die Wunschvorstellung ist eine Art Tree/Grid/Explorer View, expand/collapse, evtl. ergänzt durch diverse angemessene grafische Darstellungen. Das MVP jedoch ist primitive ASCII-Art für Einrückung und Baumstruktur, mit Textausgabe wie im ursprünglichen Kommandozeilen-Tool.

Akzeptanzkriterien

  • Die Heap-Durchwanderung muss maximal GC-freundlich operieren, um nicht eben jene Probleme zu verschärfen, die das Werkzeug zu vermeiden hilft.
  • Zwischenergebnisse müssen zudem sehr kompakt sein. Sie behalten für einen gewissen Zeitraum ihre Gültigkeit, sind aber teuer in der Erstellung; daher sollten sie gecached werden.
  • Weiter dürfen während der Ausgabephase keine gigantischen Puffer erzeugt werden.

Design

  • Die Aggregation erfolgt ähnlich wie bei Stack Traces in Profilern. Der Pfad durch den Heap-Graphen bestimmt einen Lookup Key, der fürs Einfügen und Aufaddieren in einem Trie verwendet wird.
  • In einem Nachverarbeitungsschritt wird der erzeugte Baum in eine speicherfreundliche Repräsentation überführt.
  • Der ursprünglich beobachtete Buffer Bloat lässt sich trivial vermeiden, indem man die Ausgabe direkt in einen Writer vornimmt.

Entwicklungsmuster

  • Der zweite POC entstand als Hack der ursprünglichen Codebasis. Aufgrund der unklaren Machbarkeit wurden Ideen direkt in Code ausgedrückt und im Debugger auf die Probe gestellt. So lässt sich initial gut vorankommen, bis die Grundarchitektur etabliert und die schwersten Probleme als lösbar bestätigt sind.
  • Die Vorteile des explorativen Voranpreschens verlieren sich jedoch schnell. Als gangbarer Weg bewährte sich die Absicherung des status quo per Approval Testing, und der Übergang zu Test-Driven Development (TDD) und BabySteps für die folgenden inkrementellen Verbesserungen.
  • Da die Performance hinsichtlich Laufzeit und Speicherverbrauch essenziell ist, wurde das Verhalten durch Profiling überwacht und ständig optimiert.

Ansätze zu Analyse und Optimierung

Nach wenigen Iterationen konnte das Werkzeug ohne negative Seiteneffekte produktiv eingesetzt werden. Zunächst wurde es mit sämtlichen In-Memory-Caches gefüttert, um deren Größen und Zusammensetzungen zu bestimmen. Dann stellten wir sämtliche Auffälligkeiten in Frage.

  • Große Listen und Maps, die als LinkedList bzw. TreeMap implementiert sind, können durch ArrayList bzw. HashMap effizienter gestaltet werden, wenn die Algorithmen dies gestatten.
  • Object-Arrays in Containerklassen, die viel ungenutzten Platz aufweisen, können evtl. konservativer initialisiert oder zur langfristigen Aufbewahrung auf die tatsächliche Größe gestutzt werden.
  • Der Overhead für Boxing numerischer Typen kann stellenweise durch Verwendung der Trove-Collections vermieden werden.
  • UnmodifiableMap etc. sollten durch ImmutableCollections ersetz werden, wo immer möglich.
  • Ein strikt gekapselter Copy-On-Write-Ansatz auf primitiven Arrays kann manchmal eine SynchronizedCollection ersetzen. Im Falle vieler kurzer Listen steht der Overhead für die Container in keinem Verhältnis zum Inhalt. Diese Einsparung rechtfertigt jedoch nicht notwendigerweise, die Komplexität beliebig zu erhöhen.

Praktische Ergebnisse

Durch Analyse kritischer Komponenten in der Produktivumgebung konnten wir verschiedene Stellen identifizieren, an denen der tatsächliche Speicherverbrauch stark von der Erwartungshaltung der Entwickler abwich. Der Großteil davon war nach Betrachtung des Kontexts in der Codebasis leicht durch effizientere Implementierungen zu ersetzen. In manchen Fällen konnte der Overhead sogar komplett gestrichen werden. Einsparungen von 10-30% im jeweiligen Teilbereich waren die Folge; ein Extrembeispiel konnte gar um rund 85% reduziert werden. Allein durch diese „low-hanging fruits“ ist der permanente Speicherbedarf der Anwendung bereits um ca. 10% gesunken.

Beispielausgaben

LinkedList Footprint:
           COUNT    % COUNT       AVG SZ        SUM         RAW SUM      % SUM   DESCRIPTION
              31   100.00 %           --      752 B             752   100.00 %   (total)
              10    32.26 %           24      240 B             240    31.91 %   java.util.LinkedList$Node
              10    32.26 %           24      240 B             240    31.91 %   java.lang.String
              10    32.26 %           24      240 B             240    31.91 %   [B
               1     3.23 %           32       32 B              32     4.26 %   java.util.LinkedList

LinkedList Heap Tree Drilldown:
          COUNT   AVG SIZE   RAW AVG SZ TOTAL SIZE        RAW T SZ     RETAINED CT  PAR% R CT  RETAINED SZ        RAW R SZ  PAR% R SZ   DESCRIPTION
              1       32 B           32       32 B              32              31   100.00 %        752 B             752   100.00 %   (total)
              1       32 B           32       32 B              32              31   100.00 %        752 B             752   100.00 %      +--java.util.LinkedList
             10       24 B           24      240 B             240              30    96.77 %        720 B             720    95.74 %      |  +--java.util.LinkedList$Node LinkedList.first/last
             10       24 B           24      240 B             240              20    66.67 %        480 B             480    66.67 %      |  |  +--java.lang.String Node.item
             10       24 B           24      240 B             240              10    50.00 %        240 B             240    50.00 %      |  |  |  +--[B String.value [10 of 10 used (100.00 %)]

LinkedList Class Histogram Drilldown:
          COUNT    PAR% CT   AVG SIZE   RAW AVG SZ TOTAL SIZE        RAW T SZ  PAR% T SZ   DESCRIPTION
             31   100.00 %       24 B           24      752 B             752   100.00 %   (total)
             10    32.26 %       24 B           24      240 B             240    31.91 %      +--java.util.LinkedList$Node
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--java.util.LinkedList$Node LinkedList.first/last
             10    32.26 %       24 B           24      240 B             240    31.91 %      +--java.lang.String
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--java.lang.String Node.item
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  +--java.util.LinkedList$Node LinkedList.first/last
             10    32.26 %       24 B           24      240 B             240    31.91 %      +--[B
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--[B String.value
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  +--java.lang.String Node.item
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  |  +--java.util.LinkedList$Node LinkedList.first/last
              1     3.23 %       32 B           32       32 B              32     4.26 %      +--java.util.LinkedList


ArrayList Footprint:
           COUNT    % COUNT       AVG SZ        SUM         RAW SUM      % SUM   DESCRIPTION
              22   100.00 %           --      560 B             560   100.00 %   (total)
              10    45.45 %           24      240 B             240    42.86 %   java.lang.String
              10    45.45 %           24      240 B             240    42.86 %   [B
               1     4.55 %           56       56 B              56    10.00 %   [Ljava.lang.Object;
               1     4.55 %           24       24 B              24     4.29 %   java.util.ArrayList

ArrayList Heap Tree Drilldown:
          COUNT   AVG SIZE   RAW AVG SZ TOTAL SIZE        RAW T SZ     RETAINED CT  PAR% R CT  RETAINED SZ        RAW R SZ  PAR% R SZ   DESCRIPTION
              1       24 B           24       24 B              24              22   100.00 %        560 B             560   100.00 %   (total)
              1       24 B           24       24 B              24              22   100.00 %        560 B             560   100.00 %      +--java.util.ArrayList
              1       56 B           56       56 B              56              21    95.45 %        536 B             536    95.71 %      |  +--[Ljava.lang.Object; ArrayList.elementData [10 of 10 used (100.00 %)]
             10       24 B           24      240 B             240              20    95.24 %        480 B             480    89.55 %      |  |  +--java.lang.String [i]
             10       24 B           24      240 B             240              10    50.00 %        240 B             240    50.00 %      |  |  |  +--[B String.value [10 of 10 used (100.00 %)]

ArrayList Class Histogram Drilldown:
          COUNT    PAR% CT   AVG SIZE   RAW AVG SZ TOTAL SIZE        RAW T SZ  PAR% T SZ   DESCRIPTION
             22   100.00 %       25 B           25      560 B             560   100.00 %   (total)
             10    45.45 %       24 B           24      240 B             240    42.86 %      +--java.lang.String
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--java.lang.String [i]
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  +--[Ljava.lang.Object; ArrayList.elementData
             10    45.45 %       24 B           24      240 B             240    42.86 %      +--[B
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--[B String.value
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  +--java.lang.String [i]
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  |  +--[Ljava.lang.Object; ArrayList.elementData
              1     4.55 %       56 B           56       56 B              56    10.00 %      +--[Ljava.lang.Object;
              1   100.00 %       56 B           56       56 B              56   100.00 %      |  +--[Ljava.lang.Object; ArrayList.elementData
              1     4.55 %       24 B           24       24 B              24     4.29 %      +--java.util.ArrayList


ImmutableCollections$ListN Footprint:
           COUNT    % COUNT       AVG SZ        SUM         RAW SUM      % SUM   DESCRIPTION
              22   100.00 %           --      552 B             552   100.00 %   (total)
              10    45.45 %           24      240 B             240    43.48 %   java.lang.String
              10    45.45 %           24      240 B             240    43.48 %   [B
               1     4.55 %           56       56 B              56    10.14 %   [Ljava.lang.Object;
               1     4.55 %           16       16 B              16     2.90 %   java.util.ImmutableCollections$ListN

ImmutableCollections$ListN Heap Tree Drilldown:
          COUNT   AVG SIZE   RAW AVG SZ TOTAL SIZE        RAW T SZ     RETAINED CT  PAR% R CT  RETAINED SZ        RAW R SZ  PAR% R SZ   DESCRIPTION
              1       16 B           16       16 B              16              22   100.00 %        552 B             552   100.00 %   (total)
              1       16 B           16       16 B              16              22   100.00 %        552 B             552   100.00 %      +--java.util.ImmutableCollections$ListN
              1       56 B           56       56 B              56              21    95.45 %        536 B             536    97.10 %      |  +--[Ljava.lang.Object; ListN.elements [10 of 10 used (100.00 %)]
             10       24 B           24      240 B             240              20    95.24 %        480 B             480    89.55 %      |  |  +--java.lang.String [i]
             10       24 B           24      240 B             240              10    50.00 %        240 B             240    50.00 %      |  |  |  +--[B String.value [10 of 10 used (100.00 %)]

ImmutableCollections$ListN Class Histogram Drilldown:
          COUNT    PAR% CT   AVG SIZE   RAW AVG SZ TOTAL SIZE        RAW T SZ  PAR% T SZ   DESCRIPTION
             22   100.00 %       25 B           25      552 B             552   100.00 %   (total)
             10    45.45 %       24 B           24      240 B             240    43.48 %      +--java.lang.String
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--java.lang.String [i]
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  +--[Ljava.lang.Object; ListN.elements
             10    45.45 %       24 B           24      240 B             240    43.48 %      +--[B
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--[B String.value
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  +--java.lang.String [i]
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  |  +--[Ljava.lang.Object; ListN.elements
              1     4.55 %       56 B           56       56 B              56    10.14 %      +--[Ljava.lang.Object;
              1   100.00 %       56 B           56       56 B              56   100.00 %      |  +--[Ljava.lang.Object; ListN.elements
              1     4.55 %       16 B           16       16 B              16     2.90 %      +--java.util.ImmutableCollections$ListN


ConcurrentHashMap Footprint:
           COUNT    % COUNT       AVG SZ        SUM         RAW SUM      % SUM   DESCRIPTION
              27   100.00 %           --      784 B             784   100.00 %   (total)
              10    37.04 %           24      240 B             240    30.61 %   java.lang.String
              10    37.04 %           24      240 B             240    30.61 %   [B
               5    18.52 %           32      160 B             160    20.41 %   java.util.concurrent.ConcurrentHashMap$Node
               1     3.70 %           80       80 B              80    10.20 %   [Ljava.util.concurrent.ConcurrentHashMap$Node;
               1     3.70 %           64       64 B              64     8.16 %   java.util.concurrent.ConcurrentHashMap

ConcurrentHashMap Heap Tree Drilldown:
          COUNT   AVG SIZE   RAW AVG SZ TOTAL SIZE        RAW T SZ     RETAINED CT  PAR% R CT  RETAINED SZ        RAW R SZ  PAR% R SZ   DESCRIPTION
              1       64 B           64       64 B              64              27   100.00 %        784 B             784   100.00 %   (total)
              1       64 B           64       64 B              64              27   100.00 %        784 B             784   100.00 %      +--java.util.concurrent.ConcurrentHashMap
              1       80 B           80       80 B              80              26    96.30 %        720 B             720    91.84 %      |  +--[Ljava.util.concurrent.ConcurrentHashMap$Node; ConcurrentHashMap.table [5 of 16 used (31.25 %)]
              5       32 B           32      160 B             160              25    96.15 %        640 B             640    88.89 %      |  |  +--java.util.concurrent.ConcurrentHashMap$Node [i]
             10       24 B           24      240 B             240              20    80.00 %        480 B             480    75.00 %      |  |  |  +--java.lang.String
              5       24 B           24      120 B             120              10    50.00 %        240 B             240    50.00 %      |  |  |  |  +--Node.val
              5       24 B           24      120 B             120               5    50.00 %        120 B             120    50.00 %      |  |  |  |  |  +--[B String.value [5 of 5 used (100.00 %)]
              5       24 B           24      120 B             120              10    50.00 %        240 B             240    50.00 %      |  |  |  |  +--Node.key
              5       24 B           24      120 B             120               5    50.00 %        120 B             120    50.00 %      |  |  |  |  |  +--[B String.value [5 of 5 used (100.00 %)]

ConcurrentHashMap Class Histogram Drilldown:
          COUNT    PAR% CT   AVG SIZE   RAW AVG SZ TOTAL SIZE        RAW T SZ  PAR% T SZ   DESCRIPTION
             27   100.00 %       29 B           29      784 B             784   100.00 %   (total)
             10    37.04 %       24 B           24      240 B             240    30.61 %      +--java.lang.String
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--java.util.concurrent.ConcurrentHashMap$Node
              5    50.00 %       24 B           24      120 B             120    50.00 %      |  |  +--Node.val
              5   100.00 %       24 B           24      120 B             120   100.00 %      |  |  |  +--java.util.concurrent.ConcurrentHashMap$Node [i]
              5   100.00 %       24 B           24      120 B             120   100.00 %      |  |  |  |  +--[Ljava.util.concurrent.ConcurrentHashMap$Node; ConcurrentHashMap.table
              5    50.00 %       24 B           24      120 B             120    50.00 %      |  |  +--Node.key
              5   100.00 %       24 B           24      120 B             120   100.00 %      |  |  |  +--java.util.concurrent.ConcurrentHashMap$Node [i]
              5   100.00 %       24 B           24      120 B             120   100.00 %      |  |  |  |  +--[Ljava.util.concurrent.ConcurrentHashMap$Node; ConcurrentHashMap.table
             10    37.04 %       24 B           24      240 B             240    30.61 %      +--[B
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  +--[B String.value
             10   100.00 %       24 B           24      240 B             240   100.00 %      |  |  +--java.util.concurrent.ConcurrentHashMap$Node
              5    50.00 %       24 B           24      120 B             120    50.00 %      |  |  |  +--Node.val
              5   100.00 %       24 B           24      120 B             120   100.00 %      |  |  |  |  +--java.util.concurrent.ConcurrentHashMap$Node [i]
              5   100.00 %       24 B           24      120 B             120   100.00 %      |  |  |  |  |  +--[Ljava.util.concurrent.ConcurrentHashMap$Node; ConcurrentHashMap.table
              5    50.00 %       24 B           24      120 B             120    50.00 %      |  |  |  +--Node.key
              5   100.00 %       24 B           24      120 B             120   100.00 %      |  |  |  |  +--java.util.concurrent.ConcurrentHashMap$Node [i]
              5   100.00 %       24 B           24      120 B             120   100.00 %      |  |  |  |  |  +--[Ljava.util.concurrent.ConcurrentHashMap$Node; ConcurrentHashMap.table
              5    18.52 %       32 B           32      160 B             160    20.41 %      +--java.util.concurrent.ConcurrentHashMap$Node
              5   100.00 %       32 B           32      160 B             160   100.00 %      |  +--java.util.concurrent.ConcurrentHashMap$Node [i]
              5   100.00 %       32 B           32      160 B             160   100.00 %      |  |  +--[Ljava.util.concurrent.ConcurrentHashMap$Node; ConcurrentHashMap.table
              1     3.70 %       80 B           80       80 B              80    10.20 %      +--[Ljava.util.concurrent.ConcurrentHashMap$Node;
              1   100.00 %       80 B           80       80 B              80   100.00 %      |  +--[Ljava.util.concurrent.ConcurrentHashMap$Node; ConcurrentHashMap.table
              1     3.70 %       64 B           64       64 B              64     8.16 %      +--java.util.concurrent.ConcurrentHashMap

Ausblick

  • Verbesserte User Experience durch Ausgabe in komfortabler Grid View und Visualisierung z.B. via Apache echarts.
  • Überarbeitung der Pfad-Lookup-Generierung und Erweiterung der Manipulationsmöglichkeiten für übersichtlichere Darstellungen und neuartige Ansichten.
  • Weitere Refactorings und Optimierungen der Anwendung, wo kosteneffizient oder notwendig.
  • Detailverbesserungen des Analysewerkzeugs, sowie diese aus der Verwendung hervorgehen.

Quellcode