Hallo allerseits!
Unser Experiment mit Schritten im
Java Developer- Kurs wird fortgesetzt und ist seltsamerweise sogar recht erfolgreich (sozusagen): Wie sich herausstellte, ist es viel bequemer, die Hebelwirkung von ein paar Monaten mit dem nächsten Übergang zu einem neuen Schritt zu einem geeigneten Zeitpunkt zu planen, als wenn Weisen Sie einem so schwierigen Kurs fast sechs Monate zu. Es besteht also der Verdacht, dass es genau die komplexen Kurse sind, die wir bald langsam auf ein solches System übertragen werden.
Aber es geht mir um uns, um Otusovsky, es tut mir leid. Wie immer beschäftigen wir uns weiterhin mit interessanten Themen, die zwar nicht in unserem Programm behandelt werden, aber mit uns besprochen werden. Deshalb haben wir eine Übersetzung des interessantesten Artikels unserer Meinung zu einer der Fragen vorbereitet, die unsere Lehrer gestellt haben.
Lass uns gehen!

Sammlungen im JDK sind die Standardbibliotheksimplementierungen von Listen und Karten. Wenn Sie sich den Schnappschuss einer typischen großen Java-Anwendung ansehen, sehen Sie Tausende oder sogar Millionen von Instanzen von
java.util.ArrayList
,
java.util.HashMap
usw. Sammlungen sind für das Speichern und Bearbeiten von Daten unverzichtbar. Aber haben Sie jemals darüber nachgedacht, ob alle Sammlungen in Ihrer Anwendung den Speicher optimal nutzen? Mit anderen Worten, wenn Ihre Anwendung mit dem beschämenden
OutOfMemoryError
oder lange Pausen im Garbage Collector verursacht, haben Sie die verwendeten Sammlungen jemals auf Lecks überprüft.
Zunächst sollte angemerkt werden, dass die internen Sammlungen von JDK keine Art von Magie sind. Sie sind in Java geschrieben. Der Quellcode wird mit dem JDK geliefert, sodass Sie ihn in Ihrer IDE öffnen können. Ihr Code kann auch leicht im Internet gefunden werden. Und wie sich herausstellt, sind die meisten Sammlungen nicht sehr elegant, um den Speicherbedarf zu optimieren.
Stellen Sie sich zum Beispiel eine der einfachsten und beliebtesten Sammlungen vor - die Klasse
java.util.ArrayList
. Intern arbeitet jede
ArrayList
mit einem Array von
Object[] elementData
. Hier werden die Listenelemente gespeichert. Mal sehen, wie dieses Array verarbeitet wird.
Wenn Sie eine
ArrayList
mit dem Standardkonstruktor erstellen,
elementData
new ArrayList()
elementData
, zeigt
elementData
auf ein generisches Array mit der Größe Null (
elementData
kann auch auf
null
, das Array bietet jedoch einige geringfügige Implementierungsvorteile). Wenn Sie das erste Element zur Liste hinzufügen, wird ein wirklich eindeutiges Array von
elementData
und das bereitgestellte Objekt in diese eingefügt. Um zu vermeiden, dass die Größe des Arrays beim Hinzufügen eines neuen Elements jedes Mal geändert wird, wird es mit einer Länge von 10 („Standardkapazität“) erstellt. Es stellt sich also heraus: Wenn Sie dieser
ArrayList
keine Elemente mehr hinzufügen, bleiben 9 von 10 Slots im
elementData
Array leer. Und selbst wenn Sie die Liste löschen, wird die Größe des internen Arrays nicht reduziert. Das Folgende ist ein Diagramm dieses Lebenszyklus:

Wie viel Speicher wird hier verschwendet? In absoluten Zahlen wird es berechnet als (die Größe des Objektzeigers). Wenn Sie den JVM-HotSpot verwenden (der mit dem Oracle JDK geliefert wird), hängt die Größe des Zeigers von der maximalen Heap-Größe ab (weitere Informationen finden Sie unter
https://blog.codecentric.de/de/2014/02/35gb-heap-less- 32GB-Java-JVM-Speicher-Kuriositäten / ). Wenn Sie
-Xmx
weniger als 32 Gigabyte angeben,
-Xmx
die Größe des Zeigers
-Xmx
4 Byte. für große Haufen - 8 Bytes. Somit verschwendet eine vom Standardkonstruktor initialisierte
ArrayList
mit nur einem Element entweder 36 oder 72 Bytes.
Tatsächlich verschwendet eine leere
ArrayList
auch Speicher, da sie keine Arbeitslast trägt, aber die Größe der
ArrayList
selbst ist nicht Null und größer als Sie wahrscheinlich denken. Dies liegt zum einen daran, dass ein von der HotSpot-JVM verwaltetes Objekt einerseits über einen 12- oder 16-Byte-Header verfügt, der von der JVM für interne Zwecke verwendet wird. Darüber hinaus enthalten die meisten Objekte in der Sammlung ein
size
, einen Zeiger auf ein internes Array oder ein anderes "Workload Media" -Objekt, ein
modCount
Feld zum Verfolgen von
modCount
usw. Daher wird wahrscheinlich auch das kleinstmögliche Objekt, das eine leere Sammlung darstellt, mindestens benötigt 32 Bytes Speicher. Einige, wie
ConcurrentHashMap
, nehmen viel mehr auf.
Stellen Sie sich eine andere allgemeine Sammlung vor - die Klasse
java.util.HashMap
. Sein Lebenszyklus ähnelt dem
ArrayList
Lebenszyklus:

Wie Sie sehen können,
HashMap
eine
HashMap
die nur ein Schlüssel-Wert-Paar enthält, 15 interne Zellen des Arrays, was 60 oder 120 Bytes entspricht. Diese Zahlen sind gering, aber das Ausmaß des Speicherverlusts ist für alle Sammlungen in Ihrer Anwendung wichtig. Und es stellt sich heraus, dass einige Anwendungen auf diese Weise ziemlich viel Speicher verbrauchen können. Zum Beispiel verlieren einige der beliebten Open-Source-Hadoop-Komponenten, die der Autor analysiert hat, in einigen Fällen etwa 20 Prozent ihres Heaps! Bei Produkten, die von weniger erfahrenen Ingenieuren entwickelt wurden und nicht regelmäßig überprüft werden, kann der Speicherverlust sogar noch höher sein. Es gibt genug Fälle, in denen beispielsweise 90% der Knoten in einem riesigen Baum nur ein oder zwei Nachkommen (oder gar nichts) enthalten, und andere Situationen, in denen der Heap mit 0-, 1- oder 2-Element-Sammlungen verstopft ist.
Wie können Sie unbenutzte oder nicht ausreichend genutzte Sammlungen in Ihrer Anwendung beheben? Im Folgenden finden Sie einige gängige Rezepte. Hier wird angenommen, dass unsere problematische Sammlung eine
ArrayList
auf die das Datenfeld
Foo.list
.
Wenn die meisten Instanzen der Liste nie verwendet werden, versuchen Sie, sie träge zu initialisieren. Also der Code, der vorher aussah wie ...
void addToList(Object x) { list.add(x); }
... sollte in so etwas wie erneuert werden
void addToList(Object x) { getOrCreateList().add(x); } private list getOrCreateList() {
Denken Sie daran, dass Sie manchmal zusätzliche Maßnahmen ergreifen müssen, um potenziellen Wettbewerb anzugehen. Wenn Sie beispielsweise
ConcurrentHashMap
, das von mehreren Threads gleichzeitig aktualisiert werden kann, sollte der Code, der es initialisiert, nicht zulassen, dass zwei Threads zwei Kopien dieser Map zufällig erstellen:
private Map getOrCreateMap() { if (map == null) {
Wenn die meisten Instanzen Ihrer Liste oder Karte nur wenige Elemente enthalten, versuchen Sie beispielsweise, sie mit einer geeigneteren Anfangskapazität zu initialisieren.
list = new ArrayList(4);
Wenn Ihre Sammlungen leer sind oder in den meisten Fällen nur ein Element (oder ein Schlüssel-Wert-Paar) enthalten, können Sie eine extreme Form der Optimierung in Betracht ziehen. Dies funktioniert nur, wenn die Sammlung in der aktuellen Klasse vollständig verwaltet wird, dh anderer Code kann nicht direkt darauf zugreifen. Die Idee ist, dass Sie den Typ Ihres Datenfelds beispielsweise von Liste in ein allgemeineres Objekt ändern, sodass es jetzt entweder auf eine reale Liste oder direkt auf ein einzelnes Listenelement verweisen kann. Hier ist eine kurze Skizze:
Offensichtlich ist Code mit dieser Optimierung weniger klar und schwieriger zu warten. Dies kann jedoch hilfreich sein, wenn Sie sicher sind, dass dadurch viel Speicherplatz gespart oder lange Pausen des Garbage Collectors vermieden werden.
Sie haben sich wahrscheinlich schon gefragt: Wie finde ich heraus, welche Sammlungen in meiner Anwendung Speicherplatz beanspruchen und wie viel?
Kurzum: Ohne die richtigen Werkzeuge ist es schwer herauszufinden. Der Versuch, die Menge an Speicher zu erraten, die von Datenstrukturen in einer großen komplexen Anwendung verwendet oder verbraucht wird, führt fast nie zu etwas. Und wenn Sie nicht genau wissen, wohin der Speicher geht, können Sie viel Zeit damit verbringen, die falschen Ziele zu
OutOfMemoryError
, während Ihre Anwendung mit
OutOfMemoryError
weiterhin hartnäckig
OutOfMemoryError
.
Daher sollten Sie eine Reihe von Anwendungen mit einem speziellen Tool überprüfen. Erfahrungsgemäß besteht die optimale Methode zur Analyse des JVM-Speichers (gemessen als verfügbare Informationsmenge im Vergleich zu den Auswirkungen dieses Tools auf die Anwendungsleistung) darin, einen Heap-Dump zu erstellen und ihn dann offline anzuzeigen. Ein Heap-Dump ist im Wesentlichen eine vollständige Momentaufnahme des Heaps. Sie können es jederzeit durch Aufrufen des Dienstprogramms jmap abrufen oder die JVM so konfigurieren, dass sie automatisch ausgegeben wird, wenn die Anwendung mit
OutOfMemoryError
abstürzt. Wenn Sie "JVM-Heap-Dump" googeln, sehen Sie sofort eine große Anzahl von Artikeln, in denen ausführlich erläutert wird, wie Sie einen Dump erhalten.
Ein Heap-Dump ist eine Binärdatei von der Größe eines JVM-Heaps, sodass sie nur mit speziellen Tools gelesen und analysiert werden kann. Es gibt verschiedene Tools, sowohl Open Source als auch kommerziell. Das beliebteste Open Source-Tool ist die Eclipse MAT. Es gibt auch VisualVM und einige weniger leistungsfähige und weniger bekannte Tools. Zu den kommerziellen Tools gehören universelle Java-Profiler: JProfiler und YourKit sowie ein Tool, das speziell für die Heap-Dump-Analyse entwickelt wurde - JXRay (Haftungsausschluss: zuletzt vom Autor entwickelt).
Im Gegensatz zu anderen Tools analysiert JXRay den Heap-Dump sofort auf eine Vielzahl häufiger Probleme wie wiederholte Zeilen und andere Objekte sowie auf unzureichend effiziente Datenstrukturen. Probleme mit den oben beschriebenen Sammlungen fallen in die letztere Kategorie. Das Tool generiert einen Bericht mit allen gesammelten Informationen im HTML-Format. Der Vorteil dieses Ansatzes besteht darin, dass Sie die Analyseergebnisse jederzeit und überall anzeigen und problemlos mit anderen teilen können. Sie können das Tool auch auf jedem Computer ausführen, einschließlich großer und leistungsstarker, aber „kopfloser“ Computer im Rechenzentrum.
JXRay berechnet den Overhead (wie viel Speicher Sie sparen, wenn Sie ein bestimmtes Problem beseitigen) in Bytes und als Prozentsatz des verwendeten Heaps. Es kombiniert Sammlungen derselben Klasse, die dasselbe Problem haben ...

... und gruppiert dann die problematischen Sammlungen, auf die von einem Stamm des Garbage Collectors über dieselbe Gliederkette zugegriffen werden kann, wie im folgenden Beispiel

Wenn Sie wissen, welche Verknüpfungsketten und / oder einzelnen Datenfelder (z. B.
INodeDirectory.children
oben) Sammlungen angeben, die den größten Teil ihres Speichers
INodeDirectory.children
können Sie den für das Problem verantwortlichen Code schnell und genau identifizieren und dann die erforderlichen Änderungen vornehmen.
Daher können unzureichend konfigurierte Java-Sammlungen viel Speicher verschwenden. In vielen Situationen ist dieses Problem leicht zu lösen, aber manchmal müssen Sie Ihren Code auf nicht triviale Weise ändern, um eine signifikante Verbesserung zu erzielen. Es ist sehr schwer zu erraten, welche Sammlungen optimiert werden müssen, um die größte Wirkung zu erzielen. Um keine Zeit mit der Optimierung der falschen Teile des Codes zu verschwenden, müssen Sie einen JVM-Heap-Dump erstellen und ihn mit dem entsprechenden Tool analysieren.
DAS ENDE
Wir sind wie immer an Ihren Meinungen und Fragen interessiert, die Sie hier hinterlassen oder bei
einer offenen Lektion vorbeischauen und dort unsere
Lehrer fragen können.