Die Leistung von Java ist als eine Art schwarze Magie bekannt. Ein Grund dafür ist, dass die Java-Plattform sehr komplex ist und das Problem in vielen Fällen schwer zu lokalisieren ist. Es gibt jedoch auch einen historischen Trend dahingehend, dass Menschen die Java-Leistung auf der Grundlage von Weisheit und Erfahrung untersuchen, anstatt sich auf angewandte Statistiken und empirische Überlegungen zu verlassen. In diesem Artikel möchte ich einige der lächerlichsten Tech-Mythen entlarven.
1. Java ist langsam
Es gibt viele Irrtümer über die Leistung von Java, und dieser ist der veralteteste und wahrscheinlich der offensichtlichste.
Es stimmt, dass Java in den 1990er und frühen 2000er Jahren teilweise sehr langsam war.
Seitdem haben sich die virtuellen Maschinen und die JIT-Technologie jedoch seit mehr als zehn Jahren verbessert, und die Gesamtleistung von Java ist jetzt sehr gut.
In 6 unabhängigen Web-Performance-Benchmarks landete das Java-Framework in 22 von 24 Tests unter den ersten vier.
Obwohl die JVM Leistungsprofilierung nur zur Optimierung häufig verwendeter Codepfade verwendet, ist der Optimierungseffekt offensichtlich. In vielen Fällen ist JIT-kompilierter Java-Code so schnell wie C++, und das passiert immer häufiger.
Trotzdem denken einige Leute immer noch, dass die Java-Plattform langsam ist. Dies könnte auf die historischen Vorurteile von Leuten zurückzuführen sein, die frühere Versionen der Java-Plattform erlebt haben.
Bevor wir Schlussfolgerungen ziehen, empfehlen wir, objektiv zu bleiben und die neuesten Leistungsergebnisse auszuwerten.
2. Eine einzelne Zeile Java-Code kann isoliert angezeigt werden
Betrachten Sie die folgende kurze Codezeile:
MyObject obj = new MyObject();
Ja Für einen Java-Entwickler mag es offensichtlich erscheinen, dass diese Codezeile ein Objekt zuordnet und den entsprechenden Konstruktor aufruft.
Möglicherweise können wir daraus Leistungsgrenzen ableiten. Wir gehen davon aus, dass diese Codezeile definitiv einen gewissen Arbeitsaufwand verursachen wird, und basierend auf dieser Annahme können wir versuchen, ihre Auswirkungen auf die Leistung zu berechnen.
Tatsächlich ist dieses Verständnis falsch. Es lässt uns voreingenommen sein, dass die Arbeit, egal welche Art von Arbeit, unter allen Umständen ausgeführt wird.
Tatsächlich können sowohl Javac- als auch JIT-Compiler toten Code optimieren. Was den JIT-Compiler betrifft, kann der Code basierend auf Profiling-Daten sogar durch Vorhersage optimiert werden. In diesem Fall wird diese Codezeile überhaupt nicht ausgeführt, sodass es keine Auswirkungen auf die Leistung gibt.
Darüber hinaus kann der JIT-Compiler in einigen JVMs – wie z. B. JRockit – sogar Operationen an Objekten zerlegen, sodass Zuordnungsoperationen vermieden werden können, selbst wenn der Codepfad noch gültig ist.
Die Moral hier ist, dass der Kontext beim Umgang mit Java-Leistungsproblemen sehr wichtig ist und eine vorzeitige Optimierung möglicherweise zu kontraintuitiven Ergebnissen führt. Deshalb ist es am besten, nicht vorzeitig zu optimieren. Erstellen Sie stattdessen immer Ihren Code und verwenden Sie Techniken zur Leistungsoptimierung, um Leistungs-Hotspots zu finden und diese dann zu verbessern.
3. Microbenchmarking funktioniert genau so, wie Sie denken
Wie wir oben gesehen haben, ist die Untersuchung eines kleinen Codeabschnitts nicht so genau wie die Analyse der Gesamtleistung der Anwendung.
Trotzdem lieben es Entwickler, Mikrobenchmarks zu schreiben. Es scheint, als ob es viel Spaß macht, an einigen Aspekten der zugrunde liegenden Plattform herumzubasteln.
Richard Feynman sagte einmal: „Täusche dich nicht selbst, du bist der am leichtesten zu täuschende Mensch.“ Dieser Satz ist sehr passend, um das Schreiben von Java-Mikro-Benchmarks zu veranschaulichen.
Gute Mikrobenchmarks zu schreiben ist äußerst schwierig. Die Java-Plattform ist sehr komplex und viele Mikrobenchmarks können nur vorübergehende Effekte oder andere unerwartete Aspekte der Java-Plattform messen.
Wenn Sie beispielsweise keine Erfahrung haben, messen die von Ihnen geschriebenen Mikro-Benchmarks oft nur die Zeit oder die Speicherbereinigung, erfassen aber nicht die tatsächlichen Einflussfaktoren.
Nur Entwickler und Entwicklungsteams mit echtem Bedarf sollten Mikrobenchmarks schreiben. Diese Benchmarks sollten vollständig öffentlich (einschließlich Quellcode) und reproduzierbar sein und einem Peer-Review und einer weiteren Prüfung unterliegen.
Viele Optimierungen auf der Java-Plattform zeigen, dass statistische Läufe und Einzelläufe einen großen Einfluss auf die Ergebnisse haben. Um eine echte und zuverlässige Antwort zu erhalten, sollten Sie einen einzelnen Benchmark mehrmals ausführen und die Ergebnisse dann aggregieren.
Wenn Leser das Bedürfnis verspüren, Mikro-Benchmarks zu schreiben, ist der Artikel „Statistically Rigorous Java Performance Evaluation“ von Georges, Buytaert und Eeckhout ein guter Anfang. Ohne eine ordnungsgemäße statistische Analyse können wir leicht in die Irre geführt werden.
Es gibt viele gut entwickelte Tools und Communities um sie herum (wie zum Beispiel Googles Caliper). Wenn Sie wirklich einen Mikrobenchmark schreiben müssen, schreiben Sie ihn nicht selbst. Was Sie brauchen, sind die Meinungen und Erfahrungen Ihrer Kollegen.
4. Langsame Algorithmen sind die häufigste Ursache für Leistungsprobleme
Es gibt einen sehr häufigen kognitiven Fehler unter Entwicklern (wie auch in der breiten Öffentlichkeit), das heißt, sie glauben, dass sie kontrollieren Alles im System ist wichtig.
Dieser kognitive Fehler spiegelt sich auch bei der Diskussion der Java-Leistung wider: Java-Entwickler glauben, dass die Qualität des Algorithmus die Hauptursache für Leistungsprobleme ist. Entwickler denken über Code nach und neigen daher natürlich dazu, über ihre eigenen Algorithmen nachzudenken.
Tatsächlich liegt die Wahrscheinlichkeit, dass Menschen feststellen, dass das Algorithmusdesign das grundlegende Problem ist, bei einer Reihe realer Leistungsprobleme bei weniger als 10 %.
Umgekehrt führen Garbage Collection, Datenbankzugriff und Konfigurationsfehler eher zu einer Verlangsamung der Anwendung als Algorithmen.
Die meisten Anwendungen verarbeiten relativ kleine Datenmengen, sodass selbst wenn der primäre Algorithmus ineffizient ist, dies normalerweise keine ernsthaften Leistungsprobleme verursacht. Natürlich ist unser Algorithmus nicht optimal; dennoch sind die durch den Algorithmus verursachten Leistungsprobleme immer noch relativ gering und weitere Leistungsprobleme werden durch andere Teile des Anwendungsstapels verursacht.
Unser bester Rat ist daher, tatsächliche Produktionsdaten zu verwenden, um die wahre Ursache von Leistungsproblemen aufzudecken. Leistungsdaten messen, nicht raten!
5. Caching kann alle Probleme lösen
„Alle Probleme in der Informatik können durch die Einführung einer Zwischenschicht gelöst werden.“
Dieses Zitat von David Wheeler, einem Programmierer Maxime (die mindestens zwei anderen Informatikern im Internet zugeschrieben wird) ist vor allem unter Webentwicklern sehr verbreitet.
Wenn die bestehende Architektur nicht vollständig verstanden wird und die Analyse ins Stocken geraten ist, taucht oft der Irrtum auf, dass „Caching alle Probleme lösen kann“.
In den Augen der Entwickler ist es besser, eine Caching-Ebene voranzustellen, das bestehende System zu verbergen und auf das Beste zu hoffen, anstatt sich mit dem beängstigenden bestehenden System zu befassen. Zweifellos macht dieser Ansatz die Gesamtarchitektur nur komplexer, und die Situation wird noch schlimmer, wenn der nächste Entwickler, der übernimmt, versucht, den aktuellen Status des Systems zu verstehen.
Großen, schlecht gestalteten Systemen fehlt oft das Gesamtdesign und sie werden jeweils nur eine Codezeile und ein Subsystem geschrieben. In vielen Fällen führt die Vereinfachung und Umgestaltung der Architektur jedoch zu einer besseren Leistung und ist fast immer leichter zu verstehen.
Wenn Sie also beurteilen, ob es wirklich notwendig ist, Caching hinzuzufügen, sollten Sie zunächst einige grundlegende Nutzungsstatistiken (z. B. Trefferrate und Fehlschlagsrate usw.) sammeln, um den tatsächlichen Wert der Caching-Ebene nachzuweisen .
6. Alle Anwendungen müssen auf das Stop-The-World-Problem achten.
Auf der Java-Plattform gibt es eine unveränderliche Tatsache: Um die Garbage Collection auszuführen, müssen alle Anwendungsthreads berücksichtigt werden in regelmäßigen Abständen pausiert. Dies wird manchmal als gravierender Mangel von Java angeführt, auch wenn dafür keine wirklichen Beweise vorliegen.
Empirische Untersuchungen zeigen, dass Menschen diese nicht normal wahrnehmen können, wenn sich digitale Daten (z. B. Preisschwankungen) häufiger als einmal alle 200 Millisekunden ändern.
Apps sind in erster Linie für den menschlichen Gebrauch bestimmt, daher haben wir eine nützliche Faustregel, dass ein Stop-The-World (STW) von 200 Millisekunden oder weniger normalerweise keine Auswirkungen hat. Für einige Anwendungen gelten möglicherweise höhere Anforderungen (z. B. Streaming-Medien), für viele GUI-Anwendungen ist dies jedoch nicht erforderlich.
Einige Anwendungen (z. B. Handelssysteme mit geringer Latenz oder Maschinensteuerungssysteme) tolerieren keine Pausen von 200 Millisekunden. Sofern Sie nicht diese Art von Anwendung schreiben, werden Benutzer die Auswirkungen des Garbage Collectors selten spüren.
Es ist erwähnenswert, dass in jedem System, in dem die Anzahl der Anwendungsthreads die Anzahl der physischen Kerne übersteigt, das Betriebssystem den Time-Sharing-Zugriff auf die CPU steuern muss. „Stop-The-World“ klingt beängstigend, aber tatsächlich muss sich jede Anwendung (ob JVM oder eine andere Anwendung) dem Problem der Konkurrenz um knappe Rechenressourcen stellen.
Ohne Messung ist unklar, welche zusätzlichen Auswirkungen die JVM auf die Anwendungsleistung haben wird.
Bitte schalten Sie auf jeden Fall das GC-Protokoll ein, um festzustellen, ob sich die Pausenzeit wirklich auf die Anwendung auswirkt. Bestimmen Sie die Pausenzeit, indem Sie die Protokolle analysieren, entweder manuell oder mithilfe von Skripten oder Tools. Stellen Sie dann fest, ob sie tatsächlich Probleme für die Anwendung verursachen. Stellen Sie sich vor allem eine kritische Frage: Beschweren sich Benutzer tatsächlich?
7. Der handschriftliche Objektpool ist für eine große Klasse von Anwendungen geeignet.
Da man denkt, dass Stop-The-World-Pause in gewissem Maße schlecht ist, ist eine häufige Reaktion von Anwendungsentwicklungsteams, sich selbst in Java zu implementieren Heap-Speicherverwaltungstechnologie. Dies läuft oft auf die Implementierung eines Objektpools (oder sogar auf eine vollständige Referenzzählung) hinaus und erfordert die Einbindung von Code, der Domänenobjekte verwendet.
Diese Technik ist fast immer irreführend. Es basiert auf dem Verständnis der Vergangenheit, als die Objektzuweisung sehr teuer und die Änderung von Objekten viel billiger war. Die Dinge sind jetzt völlig anders.
Heutige Hardware ist bei der Zuweisung sehr effizient; mit der neuesten Desktop- oder Server-Hardware beträgt die Speicherbandbreite mindestens 2 bis 3 GB. Dies ist eine große Zahl, und es ist nicht einfach, eine so große Bandbreite vollständig zu nutzen, es sei denn, die Anwendung ist speziell geschrieben.
Im Allgemeinen ist es sehr schwierig, Objekt-Pooling korrekt zu implementieren (insbesondere, wenn mehrere Threads arbeiten), und Objekt-Pooling bringt auch einige negative Anforderungen mit sich, die diese Technik generell nicht zu einer guten Wahl machen:
Alle Entwickler, die mit dem Objektpool-Code in Berührung kommen, müssen den Objektpool verstehen und richtig damit umgehen können
Welcher Code kennt den Objektpool und welcher Code kennt den Objektpool, die Grenzen nicht muss allen bekannt und im Dokument vermerkt sein
Diese zusätzlichen Komplexitäten sollten regelmäßig aktualisiert und überprüft werden
Wenn eine davon nicht erfüllt ist, besteht die Gefahr, dass Probleme stillschweigend auftreten ( ähnlich der Wiederverwendung von Zeigern in C) Es ist wieder da
Kurz gesagt, der Objektpool kann nur verwendet werden, wenn die GC-Pausen inakzeptabel sind und Optimierung und Rekonstruktion es nicht schaffen, die Pausen auf ein akzeptables Maß zu reduzieren.
8. Im Vergleich zu Parallel Old ist CMS immer die bessere Wahl
Oracle JDK verwendet standardmäßig einen parallelen Stop-The-World-Collector, um die alte Generation zu sammeln , Paralleler alter Sammler.
Concurrent-Mark-Sweep (CMS) ist eine Alternative, die es Anwendungsthreads ermöglicht, während der meisten Garbage-Collection-Zyklen weiterzulaufen, ist jedoch mit Kosten verbunden und weist einige Einschränkungen auf.
Das Zulassen, dass der Anwendungsthread zusammen mit dem Garbage-Collection-Thread ausgeführt wird, führt zwangsläufig zu einem Problem: Der Anwendungsthread ändert den Objektgraphen, was sich auf die Lebensfähigkeit des Objekts auswirken kann. Diese Situation muss im Nachhinein bereinigt werden, sodass das CMS tatsächlich zwei STW-Phasen hat (normalerweise sehr kurz).
Dies wird einige Konsequenzen haben:
Alle Anwendungsthreads müssen an einen sicheren Punkt gebracht werden und werden während jedes vollständigen GC zweimal angehalten
Obwohl Garbage Collection und Anwendung Gleichzeitig ausführen, aber der Anwendungsdurchsatz wird reduziert (normalerweise 50 %);
Bei Verwendung von CMS für die Speicherbereinigung sind die von der JVM verwendeten Buchhaltungsinformationen (und CPU-Zyklen) viel höher als bei anderen parallelen Anwendungen Sammler.
Ob sich diese Kosten lohnen, hängt von der Anwendung ab. Aber es gibt kein kostenloses Mittagessen. Der CMS-Kollektor ist in seinem Design lobenswert, aber kein Allheilmittel.
Bevor Sie also bestätigen, dass CMS die richtige Garbage-Collection-Strategie ist, sollten Sie zunächst bestätigen, dass die STW-Pause von Parallel Old tatsächlich inakzeptabel ist und nicht angepasst werden kann. Abschließend möchte ich betonen, dass alle Metriken von einem System bezogen werden müssen, das dem Produktionssystem entspricht.
9. Durch Erhöhen der Heap-Größe können Speicherprobleme gelöst werden.
Wenn eine Anwendung in Schwierigkeiten ist und ein GC-Problem vermutet wird, besteht die Reaktion vieler Anwendungsteams darin, die Heap-Größe zu erhöhen. In manchen Fällen führt dies zu schnellen Ergebnissen und gibt uns Zeit, über durchdachtere Lösungen nachzudenken. Wenn die Ursache des Leistungsproblems jedoch nicht vollständig geklärt ist, kann diese Strategie die Situation verschlimmern.
Stellen Sie sich eine sehr schlecht codierte Anwendung vor, die viele Domänenobjekte erzeugt (ihre Lebensdauer beträgt beispielsweise 2-3 Sekunden). Wenn die Zuweisungsrate hoch genug ist, kommt es häufig zu einer Speicherbereinigung, sodass Domänenobjekte auf die alte Generation heraufgestuft werden. Fast sobald Domänenobjekte in die alte Generation eintreten, endet ihre Überlebenszeit und sie sterben direkt, werden jedoch erst beim nächsten vollständigen GC recycelt.
Wenn Sie die Heap-Größe Ihrer Anwendung erhöhen, vergrößern wir lediglich den Platz, den relativ kurzlebige Objekte zum Eintreten und Absterben benötigen. Dadurch wird die Stop-The-World-Pausenzeit länger, was für die Anwendung nicht vorteilhaft ist.
Bevor Sie die Heap-Größe ändern oder andere Parameter anpassen, müssen Sie die Dynamik der Objektzuweisung und -lebensdauer verstehen. Blindes Handeln ohne Messung von Leistungsdaten wird die Situation nur verschlimmern. Besonders wichtig ist hier die alte Generationsverteilung des Garbage Collectors.
Fazit
Wenn es um die Leistungsoptimierung in Java geht, täuscht die Intuition oft. Wir benötigen experimentelle Daten und Tools, die uns helfen, das Verhalten der Plattform zu visualisieren und unser Verständnis zu verbessern.
Die Müllabfuhr ist das beste Beispiel. Für die Optimierung oder die Generierung von Daten zur Steuerung der Optimierung bietet das GC-Subsystem unbegrenzte Möglichkeiten. Für Produktionsanwendungen ist es jedoch schwierig, die Bedeutung der generierten Daten ohne den Einsatz von Werkzeugen zu verstehen.
Standardmäßig sollten Sie beim Ausführen eines Java-Prozesses (einschließlich Entwicklungsumgebung und Produktionsumgebung) immer mindestens die folgenden Parameter verwenden:
-verbose:gc (GC-Protokoll drucken)
- >
Verwenden Sie dann Tools zur Analyse der Protokolle. Hier können Sie handgeschriebene Skripte verwenden, Diagramme erstellen oder Visualisierungstools wie GCViewer (Open Source) oder jClarity Censum verwenden.