So verhindern Sie einen Speicherüberlauf bei Verwendung von Java-Sammlungen

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() { //   ,         if (list == null) list = new ArrayList(); return list; } 

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) { //,       synchronized (this) { if (map == null) map = new ConcurrentHashMap(); } } return map; } 

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); //       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:

 // ***   *** private List<Foo> list = new ArrayList<>(); void addToList(Foo foo) { list.add(foo); } // ***   *** //   ,    null.      , //      .       //   ArrayList. private Object listOrSingleEl; void addToList(Foo foo) { if (listOrSingleEl == null) { //   listOrSingleEl = foo; } else if (listOrSingleEl instanceof Foo) { //  Foo firstEl = (Foo) listOrSingleEl; ArrayList<Foo> list = new ArrayList<>(); listOrSingleEl = list; list.add(firstEl); list.add(foo); } else { //      ((ArrayList<Foo>) listOrSingleEl).add(foo); } } 

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.

Source: https://habr.com/ru/post/de420245/


All Articles